{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "nix-book",
  "home_page_url": "https://saylesss88.github.io/",
  "feed_url": "https://saylesss88.github.io/feed.json",
  "description": "Description",
  "items": [
    {
      "id": "https://saylesss88.github.io/Understanding_Nix_Functions_2.html",
      "url": "https://saylesss88.github.io/Understanding_Nix_Functions_2.html",
      "title": "Understanding Nix Functions",
      "content_html": "<h1>Chapter 2</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- <img src=\"https://saylesss88.github.io/images/nixLogo.png\" width=\"400\" height=\"300\"> -->\n<p><img src=\"https://saylesss88.github.io/images/trees2.cleaned.png\" alt=\"trees2\" /></p>\n<h2>Understanding Nix Functions</h2>\n<p><strong>Functions</strong> are the building blocks of Nix, appearing everywhere in Nix\nexpressions and configurations. Mastering them is essential for writing\neffective Nix code and understanding tools like NixOS and Home Manager. This\nchapter explores how Nix functions work, focusing on their <strong>single-argument\nnature</strong>, <strong>currying</strong>, <strong>partial application</strong>, and their role in <strong>modules</strong>.</p>\n<h2>What are Nix Functions?</h2>\n<p>A <strong>Nix Function</strong> is a rule that takes an input (called an <strong>argument</strong>) and\nproduces an <strong>output</strong> based on that input. Unlike many programming languages,\nNix functions are designed to take exactly one argument at a time. This unique\napproach, combined with a technique called currying, allows Nix to simulate\nmulti-argument functions in a flexible and reusable way.</p>\n<h2>Builtins</h2>\n<details>\n<summary> ✔️ Nix Builtin Functions (Click to Expand)</summary>\n<p>The Nix expression evaluator has a bunch of functions and constants built in:</p>\n<ul>\n<li>\n<p><code>toString e</code>: (Convert the expression <code>e</code> to a string)</p>\n</li>\n<li>\n<p><code>import path</code>: (Load, parse and return the Nix expression in the file <code>path</code>)</p>\n</li>\n<li>\n<p><code>throw x</code>: (Throw an error message <code>x</code>. Usually stops evaluation)</p>\n</li>\n<li>\n<p><code>map f list</code>: (Apply the function <code>f</code> to each element in the <code>list</code>)</p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.18/language/builtins\">Built-in Functions</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.26/language/operators\">Nix Operators</a></p>\n</li>\n</ul>\n</details>\n<h2>Lambdas</h2>\n<p>Nix functions are anonymous (lambdas) (e.g., <code>x: x + 2</code>), and technically take a\nsingle parameter. However, that single parameter is very often an attribute set,\nallowing you to effectively pass multiple named inputs by destructuring (e.g.,\n<code>{ arg1, arg2 }: arg1 + arg2</code>).</p>\n<p>Type the parameter name, followed by a colon, and finally the body of the\nfunction.</p>\n<pre><code class=\"language-nix\">nix-repl&gt; param: param * 2\n&lt;&lt;lambda @ &lt;&lt;string&gt;&gt;:1:1&gt;&gt;\n\nnix-repl&gt; (param: param * 2) 2\n4\n</code></pre>\n<p>The above example shows that everything in Nix returns a value. When you call a\nfunction directly (without first assigning the function itself to a variable),\nthe result of that call is immediately evaluated and displayed/used.</p>\n<p>In order to make our function reusable and be able to pass different values at\ndifferent times we have to assign our function to a variable:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; twoTimes = param: param * 2\n</code></pre>\n<p>Now, we can reference our function by it’s name and pass our required parameter:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; twoTimes\n«lambda @ «string»:1:2»\nnix-repl&gt; twoTimes 2\n4\nnix-repl&gt; twoTimes 4\n8\n</code></pre>\n<p>We defined a function <code>param: param * 2</code> takes one parameter <code>param</code>, and\nreturns <code>param * 2</code>. We then assigned this function to the variable <code>twoTimes</code>.\nLastly, we called the function with a few different arguments showing it’s\nreusability.</p>\n<h2>Understanding Function Structure: The Role of the Colon</h2>\n<p>The colon (<code>:</code>) acts as a clear separator within a function definition:</p>\n<ul>\n<li>\n<p><strong>Left of the Colon:</strong> This is the function’s <strong>argument</strong>. It’s a placeholder\nname for a value that will be provided when the function is called.</p>\n</li>\n<li>\n<p><strong>Right of the Colon:</strong> This is the <strong>function body</strong>. It’s the expression\nthat will be evaluated when the function is invoked.</p>\n</li>\n</ul>\n<p><strong>Think of function arguments as naming values that aren’t known in advance.</strong>\nThese names are placeholders that get filled with specific values when the\nfunction is used.</p>\n<p><strong>Example:</strong></p>\n<pre><code class=\"language-nix\">greet = personName: \"Hello, ${personName}!\";\n</code></pre>\n<ul>\n<li>\n<p>Here, <code>personName</code> is the <strong>argument</strong> (the placeholder).</p>\n</li>\n<li>\n<p><code>\"Hello, ${personName}!\"</code>, is the <strong>function body</strong> (which uses the\nplaceholder to create the greeting).</p>\n</li>\n</ul>\n<p>When you call the function, (click to see Output):</p>\n<pre><code class=\"language-nix\">greet \"Anonymous\"\n~ \"Hello, Anonymous!\"\n</code></pre>\n<ul>\n<li>\n<p>The value <code>\"Anonymous\"</code> is substituted for the <code>personName</code> placeholder within\nthe function body.</p>\n</li>\n<li>\n<p>This structure is the foundation of all Nix functions, whether simple or\ncomplex.</p>\n</li>\n</ul>\n<h3>Single-Argument Functions: The Basics</h3>\n<p>The simplest form of a Nix function takes a single argument. In Nix, function\ndefinitions like <code>x: x + 1</code> or <code>personName: \"Hello, ${personName}!\";</code> are\n<strong>anonymous lambda functions</strong>. They exist as values until they are assigned to\na variable.</p>\n<ul>\n<li>Click to see Output:</li>\n</ul>\n<pre><code class=\"language-nix\"># This is an anonymous lambda function value:\n# x: x + 1\ninc = x: x + 1;          # here we assigned our lambda to a variable `inc`\ninc 5\n~ 6\n</code></pre>\n<ul>\n<li>\n<p><code>x</code> is the argument.</p>\n</li>\n<li>\n<p><code>x + 1</code> is the function body.</p>\n</li>\n</ul>\n<p>This straightforward design makes single-argument functions easy to understand\nand use. But what if you need a function that seems to take multiple arguments?\nThat’s where <strong>currying</strong> comes in.</p>\n<h3>Simulating Multiple Arguments: Currying</h3>\n<p>To create functions that appear to take multiple arguments, Nix uses currying.\nThis involves nesting single-argument functions, where each function takes one\nargument and returns another function that takes the next argument, and so on.</p>\n<pre><code class=\"language-nix\">nix-repl&gt; multiply = x: (y: x*y)\nnix-repl&gt; multiply\n«lambda»\nnix-repl&gt; multiply 4\n«lambda»\nnix-repl&gt; (mul 4) 5\n20\n</code></pre>\n<p>We defined a function that takes the parameter <code>x</code>, the body returns another\nfunction. This other function takes a parameter <code>y</code> and returns <code>x*y</code>.\nTherefore, calling <code>multiply 4</code> returns a function like: <code>x: 4*y</code>. In turn, we\ncall the returned function with <code>5</code>, and get the expected result.</p>\n<h4>Currying example 2</h4>\n<pre><code class=\"language-nix\"># concat is equivalent to:\n# concat = x: (y: x + y);\nconcat = x: y: x + y;\nconcat 6 6    # Evaluates to 12\n12\n</code></pre>\n<p>Here, <code>concat</code> is actually <strong>two nested functions</strong></p>\n<ol>\n<li>\n<p>The <strong>first function</strong> takes <code>x</code> and returns another function.</p>\n</li>\n<li>\n<p>The <strong>second function</strong> takes <code>y</code> and performs <code>x + y</code></p>\n</li>\n</ol>\n<p>Nix interprets the colons (<code>:</code>) as separators for this chain of single-argument\nfunctions.</p>\n<p>Here’s how it works step by step:</p>\n<ul>\n<li>\n<p>When you call <code>concat 6</code>, the outer function binds <code>x</code> to <code>6</code> and returns a\nnew function: <code>y: 6 + y</code>.</p>\n</li>\n<li>\n<p>When you call that function with <code>6</code> (i.e., <code>concat 6 6</code>), it computes\n<code>6 + 6</code>, resulting in <code>12</code>.</p>\n</li>\n</ul>\n<p>This chaining is why Nix functions are so powerful—it allows you to build\nflexible, reusable functions.</p>\n<p>Currying is a powerful feature in Nix that enables you to partially apply\narguments to functions, leading to increased reusability. This behavior is a\ndirect consequence of Nix functions being “first-class citizens” (a concept\nwe’ll delve into later), and it proves invaluable for decomposing intricate\nlogic into a series of smaller, more focused functions.</p>\n<p><strong>Key Insight</strong>: Every colon in a function definition separates a <strong>single\nargument</strong> from its <strong>function body</strong>, even if that body is another function\ndefinition.</p>\n<h4>Greeting Example</h4>\n<p>Let’s explore currying with a more relatable example in the <code>nix repl</code>:</p>\n<pre><code class=\"language-nix\">nix repl\nnix-repl&gt; greeting = prefix: name: \"${prefix}, ${name}!\";\n\nnix-repl&gt; greeting \"Hello\"\n&lt;&lt;lambda @ &lt;&lt;string&gt;&gt;:1:10&gt;&gt; # partial application returns a lambda\n\nnix-repl&gt; greeting \"Hello\" \"Alice\"\n\"Hello, Alice!\"         # providing both arguments returns the expected result\n</code></pre>\n<p>This function is a chain of two single-argument functions:</p>\n<ol>\n<li>\n<p>The outer function takes <code>prefix</code> (e.g. <code>\"Hello\"</code>) and returns a function\nthat expects <code>name</code>.</p>\n</li>\n<li>\n<p>The inner function takes <code>name</code> (e.g. <code>\"Alice\"</code>) and combines it with\n<code>prefix</code> to produce the final string.</p>\n</li>\n</ol>\n<p>Thanks to <strong>lexical scope</strong> (where inner functions can access variables from\nouter functions), the inner function “remembers” the <code>prefix</code> value.</p>\n<h4>Partial Application: Using Functions Incrementally</h4>\n<p>Because of <strong>currying</strong>, you can apply arguments to a Nix function one at a\ntime. This is called <em>partial application</em>. When you provide only some of the\nexpected arguments, you get a new function that “remembers” the provided\narguments and waits for the rest.</p>\n<blockquote>\n<p>[!EXAMPLE]</p>\n<p>Using our <code>greeting</code> function again:</p>\n<pre><code class=\"language-nix\">nix repl\nnix-repl&gt; greeting = prefix: name: \"${prefix}, ${name}!\";\nnix-repl&gt; helloGreeting = greeting \"Hello\";\nnix-repl&gt; helloGreeting \"Alice\"\n\"Hello, Alice\"\n</code></pre>\n<ul>\n<li><code>helloGreeting</code> is now a new function. It has already received the <code>prefix</code>\nargument (<code>\"Hello\"</code>), when we provide the second argument we get\n<code>\"Hello, Alice!\"</code></li>\n</ul>\n</blockquote>\n<p><strong>Benefits of Partial Application:</strong></p>\n<p>Partial application provides significant benefits by enabling you to derive\nspecialized functions from more general ones through the process of fixing\ncertain parameters. Additionally, it serves as a powerful tool for adapting\nexisting functions to fit the precise argument requirements of higher-order\nfunctions like <code>map</code> and <code>filter</code>.</p>\n<h4>Nix Functions being “first class citizens”</h4>\n<p>In the context of Nix, the phrase “Nix treats functions as first-class citizens”\nmeans that functions in Nix are treated as values, just like numbers, strings,\nor lists. They can be manipulated, passed around, and used in the same flexible\nways as other data types. This concept comes from functional programming and has\nspecific implications in Nix.</p>\n<p><strong>What It Means in Nix</strong></p>\n<ol>\n<li>Functions Can Be <strong>Assigned to Variables</strong>:</li>\n</ol>\n<ul>\n<li>You can store a function in a variable, just like you would store a number or\nstring.</li>\n</ul>\n<blockquote>\n<p>[!EXAMPLE]</p>\n<pre><code class=\"language-nix\">greet = name: \"Hello, ${name}!\";\n</code></pre>\n<ul>\n<li>Here, greet is a variable that holds a function.</li>\n</ul>\n</blockquote>\n<ol start=\"2\">\n<li>Functions Can Be <strong>Passed as Arguments</strong>:</li>\n</ol>\n<ul>\n<li>You can pass a function to another function as an argument, allowing for\nhigher-order functions (functions that operate on other functions).</li>\n</ul>\n<blockquote>\n<p>[!EXAMPLE]</p>\n<pre><code class=\"language-nix\">applyTwice = f: x: f (f x);\ninc = x: x + 1;\napplyTwice inc 5 # Output: 7 (increments 5 twice: 5 → 6 → 7)\n~ 7\n</code></pre>\n<ul>\n<li>Here, applyTwice takes a function <code>f</code> (in this case, <code>inc</code>) and applies it to\n<code>x</code> twice.</li>\n</ul>\n</blockquote>\n<ol start=\"3\">\n<li>Functions Can Be <strong>Returned from Functions</strong>:</li>\n</ol>\n<ul>\n<li>Functions can produce other functions as their output, which is key to\ncurrying in Nix.</li>\n</ul>\n<blockquote>\n<p>[!EXAMPLE]</p>\n<pre><code class=\"language-nix\">greeting = prefix: name: \"${prefix}, ${name}!\";\nhelloGreeting = greeting \"Hello\";  # Returns a function\nhelloGreeting \"Alice\"  # Output: \"Hello, Alice!\"\n~ \"Hello, Alice!\"\n</code></pre>\n<ul>\n<li>The greeting function returns another function when partially applied with\nprefix.</li>\n</ul>\n</blockquote>\n<ol start=\"4\">\n<li>Functions <strong>Are Values in Expressions</strong>:</li>\n</ol>\n<ul>\n<li>Functions can be used anywhere a value is expected, such as in attribute sets\nor lists.</li>\n</ul>\n<blockquote>\n<p>[!EXAMPLE]</p>\n<pre><code class=\"language-nix\">myFuncs = {\n  add = x: y: x + y;\n  multiply = x: y: x * y;\n};\nmyFuncs.add 3 4  # Output: 7\n~ 7\n</code></pre>\n<ul>\n<li>\n<p>Here, functions are stored as values in an attribute set.</p>\n</li>\n<li>\n<p>To try this in the <code>repl</code> just remove the semi-colon (<code>;</code>)</p>\n</li>\n</ul>\n</blockquote>\n<p><strong>Why This Matters in Nix</strong>:</p>\n<p>This functional approach is fundamental to Nix’s unique build system. In Nix,\n<strong>package builds (called derivations)</strong> are essentially functions. They take\nspecific <strong>inputs</strong> (source code, dependencies, build scripts) and\ndeterministically produce <strong>outputs</strong> (a built package).</p>\n<p>This design ensures <strong>atomicity</strong>: if a build does not succeed completely and\nperfectly, it produces no output at all. This prevents situations common in\nother package managers where partial updates or corrupted builds can leave your\nsystem in an inconsistent or broken state.</p>\n<p>Many NixOS and Home Manager modules are functions, and their first-class status\nmeans they can be combined, reused, or passed to other parts of the\nconfiguration system.</p>\n<p>Now that we understand the “first-class” nature of Nix Functions let’s see how\nthey fit into NixOS and Home Manager modules.</p>\n<h4>The Function Nature of NixOS and Home Manager Modules</h4>\n<p>It’s crucial to understand that most NixOS and Home Manager modules are\nfundamentally <strong>functions</strong>.</p>\n<p>These module functions typically accept a single argument: <strong>an attribute set</strong>\n(remember this, it’s important to understand).</p>\n<p><strong>Example</strong>:</p>\n<p>A practical NixOS module example for Thunar with plugins:</p>\n<pre><code class=\"language-nix\"># thunar.nix\n{pkgs, ...}: {\n  programs = {\n    thunar = {\n      enable = true;\n      plugins = with pkgs.xfce; [\n        thunar-archive-plugin\n        thunar-volman\n      ];\n    };\n  };\n}\n</code></pre>\n<ul>\n<li>To use this module I would need to import it into my <code>configuration.nix</code> or\nequivalent, shown here for completeness.</li>\n</ul>\n<pre><code class=\"language-nix\"># configuration.nix\n# ... snip ...\nimports = [ ../nixos/thunar.nix ];\n# ... snip ...\n</code></pre>\n<ul>\n<li>\n<p>This is actually a pretty good example of <code>with</code> making it a bit harder to\nreason where the plugins are from. You might instinctively try to trace a path\nlike <code>programs.thunar.plugins.pkgs.xfce</code> because you saw <code>pkgs.xfce</code> in the\n<code>with</code> statement. But that’s now how <code>with</code> works. The <code>pkgs.xfce</code> path exists\n<em>outside</em> the <code>plugins</code> list, defining the source of the items, not their\nnested structure within the list.</p>\n</li>\n<li>\n<p>To follow best practices you could write the above plugins section as:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">plugins = [\n  pkgs.xfce.thunar-archive-plugin\n  pkgs.xfce.thunar-volman\n];\n</code></pre>\n<ul>\n<li>Now it’s clear that each plugin comes directly from <code>pkgs</code> and each will\nresolve to a derivation.\n<ul>\n<li>To be clear either way is fine, especially in such a small self contained\nmodule. If it were in a single file <code>configuration.nix</code> it would be a bit\nmore confusing to trace. Explicitness is your friend with Nix and\nmaintaining reproducability. <code>with</code> isn’t always bad but should be avoided\nat the top of a file for example to bring <code>nixpkgs</code> into scope, use <code>let</code>\ninstead.</li>\n</ul>\n</li>\n</ul>\n<p>The entire module definition is a function that takes one argument (an attribute\nset):<code>{ pkgs, ... }</code>. When this module is included in your configuration, the\nNixOS module system calls this function with a specific attribute set. This\nattribute set contains the available packages (<code>pkgs</code>), and other relevant\ninformation. The module then uses these values to define parts of your system.</p>\n<h3>Understanding passing and getting back arguments</h3>\n<p>For this example we will build the Hello derivation from the Nix Pills series.</p>\n<p>Create an <code>autotools.nix</code> with the following contents:</p>\n<pre><code class=\"language-nix\">pkgs: attrs: let\n  defaultAttrs = {\n    builder = \"${pkgs.bash}/bin/bash\";\n    args = [./builder.sh];\n    baseInputs = with pkgs; [\n      gnutar\n      gzip\n      gnumake\n      gcc\n      coreutils\n      gawk\n      gnused\n      gnugrep\n      binutils.bintools\n    ];\n    buildInputs = [];\n    system = builtins.currentSystem;\n  };\nin\n  derivation (defaultAttrs // attrs)\n</code></pre>\n<p>Let’s create the hello derivation:</p>\n<pre><code class=\"language-nix\">let\n  pkgs = import &lt;nixpkgs&gt; {};\n  mkDerivation = import ./autotools.nix pkgs;\nin\n  mkDerivation {\n    name = \"hello\";\n    src = ./hello-2.12.1.tar.gz;\n  }\n</code></pre>\n<ul>\n<li>You can get the tarball\n<a href=\"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\">here</a>, place it in the\nsame directory as <code>autotools.nix</code></li>\n</ul>\n<p>And finally the <code>builder.sh</code> that <code>autotools.nix</code> declares for the <code>args</code>\nattribute:</p>\n<pre><code class=\"language-bash\">#!/bin/bash\nset -e\nunset PATH\nfor p in $buildInputs $baseInputs; do\n    export PATH=$p/bin${PATH:+:}$PATH\ndone\n\ntar -xf $src\n\nfor d in *; do\n    if [ -d \"$d\" ]; then\n        cd \"$d\"\n        break\n    fi\ndone\n\n./configure --prefix=$out\nmake\nmake install\n</code></pre>\n<p>When you write:</p>\n<pre><code class=\"language-nix\">mkDerivation = import ./autotools.nix pkgs;\n</code></pre>\n<ul>\n<li>\n<p><code>import ./autotools.nix</code>: This evaluates the <code>autotools.nix</code> file. Because it\nstarts with <code>pkgs: attrs: ...</code>, it means that <code>autotools.nix</code> evaluates to a\nfunction that expects one argument named <code>pkgs</code>.</p>\n</li>\n<li>\n<p><code>... pkgs</code>: We are immediately calling that function (the one returned by\n<code>import ./autotools.nix</code>) and passing it our <code>pkgs</code> variable (which is the\nresult of <code>import &lt;nixpkgs&gt; {}</code>).</p>\n</li>\n</ul>\n<p><strong>This illustrates the concept of Currying in Nix</strong>:</p>\n<p>The function defined in <code>autotools.nix</code> (<code>pkgs: attrs: ...</code>) is a curried\nfunction. It’s a function that, when given its first argument (<code>pkgs</code>), returns\nanother function (which then expects <code>attrs</code>).</p>\n<p>The result of import <code>./autotools.nix pkgs</code> is that second, inner function:\n<code>attrs: derivation (defaultAttrs // attrs)</code>. This inner function is then bound\nto the <code>mkDerivation</code> variable, making it ready to be called with just the\nspecific attributes for your package (like <code>name</code> and <code>src</code>).</p>\n<p><strong>Understanding the <code>attrs</code> Argument</strong></p>\n<p>Now let’s focus on the second argument of our <code>autotools.nix</code> function: <code>attrs</code>.</p>\n<p>Recall the full function signature in <code>autotools.nix</code>:</p>\n<pre><code class=\"language-nix\">pkgs: attrs: let\n  # ... defaultAttrs definition ...\nin\n  derivation (defaultAttrs // attrs)\n</code></pre>\n<ol>\n<li>What <code>attrs</code> Represents:</li>\n</ol>\n<ul>\n<li>\n<p>Once <code>autotools.nix</code> has received its <code>pkgs</code> argument (and returned the inner\nfunction), this inner function is waiting for its final argument, which we\ncall <code>attrs</code>.</p>\n</li>\n<li>\n<p><code>attrs</code> is simply an attribute set (a key-value map in Nix). It’s designed to\nreceive all the specific properties of the individual package you want to\nbuild using this helper.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li>How <code>attrs</code> is Used:</li>\n</ol>\n<ul>\n<li>\n<p>Look at the final line of <code>autotools.nix</code>:\n<code>derivation (defaultAttrs // attrs)</code>.</p>\n</li>\n<li>\n<p>The <code>//</code> operator in Nix performs an attribute set merge. It takes all\nattributes from <code>defaultAttrs</code> and combines them with all attributes from\n<code>attrs</code>.</p>\n</li>\n<li>\n<p>Crucially, if an attribute exists in both <code>defaultAttrs</code> and <code>attrs</code>, the\nvalue from <code>attrs</code> (the second operand) takes precedence and overrides the\ndefault value.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Applying attrs in the hello Derivation:</li>\n</ol>\n<ul>\n<li>In the <code>hello</code> derivation, we call <code>mkDerivation</code> like this:</li>\n</ul>\n<pre><code class=\"language-nix\">        mkDerivation {\n          name = \"hello\";\n          src = ./hello-2.12.1.tar.gz;\n        }\n</code></pre>\n<ul>\n<li>\n<p>The attribute set <code>{ name = \"hello\"; src = ./hello-2.12.1.tar.gz; }</code> is what\ngets passed as the <code>attrs</code> argument to the <code>mkDerivation</code> function (which,\nremember, is the inner function returned by <code>autotools.nix</code>).</p>\n</li>\n<li>\n<p>When derivation <code>(defaultAttrs // attrs)</code> is evaluated for “hello”, the <code>name</code>\nand <code>src</code> provided in the <code>attrs</code> set will be merged with all the\n<code>defaultAttrs</code> (like <code>builder</code>, <code>args</code>, <code>baseInputs</code>, etc.).</p>\n</li>\n</ul>\n<p>In summary:</p>\n<ul>\n<li>\n<p>The <code>pkgs</code> argument configures the general environment and available tools for\nthe builder.</p>\n</li>\n<li>\n<p>The <code>attrs</code> argument is where you provide the unique details for each specific\npackage you intend to build using this <code>autotools.nix</code> helper. It allows you\nto specify things like the package’s name, source code, version, and any\ncustom build flags, while still benefiting from all the sensible defaults\nprovided by <code>autotools.nix</code>. This separation makes <code>autotools.nix</code> a reusable\nand flexible “template” for creating derivations.</p>\n</li>\n</ul>\n<h4>Conclusion</h4>\n<p>Having explored the fundamental nature of functions in Nix, we can now see this\nconcept applies to more complex areas like NixOS configuration and derivations.\nIn the next chapter,\n<a href=\"https://saylesss88.github.io/NixOS_Modules_Explained_3.html\">NixOS Modules Explained</a>.\nWe will learn about NixOS Modules which are themselves functions most of the\ntime.</p>\n<h4>Resources</h4>\n<details>\n<summary> ✔️ Resources (Click to Expand) </summary>\n<ul>\n<li>\n<p><a href=\"https://nix.dev/tutorials/nix-language.html\">nix.dev Nix Lang Basics</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/05-functions-and-imports.html\">nix pills Functions and Imports</a></p>\n</li>\n<li>\n<p><a href=\"https://zero-to-nix.com/concepts/nix-language/\">zero-to-nix Nix Lang</a></p>\n</li>\n<li>\n<p><a href=\"https://nixcloud.io/tour/?id=functions%2Fintroduction\">A tour of Nix “Functions”</a></p>\n</li>\n<li>\n<p><a href=\"https://learnxinyminutes.com/nix/\">learn Nix in y minutes</a></p>\n</li>\n<li>\n<p><a href=\"https://noogle.dev/\">noogle function library</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2026-08-01T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/hardening_NixOS.html",
      "url": "https://saylesss88.github.io/nix/hardening_NixOS.html",
      "title": "Hardening NixOS",
      "content_html": "<h1>Hardening NixOS</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/../images/guy_fawks.png\" alt=\"guy fawks hacker\" /></p>\n<p>Securing your NixOS system begins with a philosophy of minimalism, explicit\nconfiguration, and proactive control. As desktop Linux attracts more novice\nusers, it has become an increasingly valuable target for attackers. This makes\nit crucial to adopt security best practices early to protect your desktop from\ncommon attack vectors and to avoid configuration mistakes that could expose\nvulnerabilities.</p>\n<blockquote class=\"markdown-alert-warning\">\n<p>I am not a security expert. This guide presents various options for hardening\nNixOS, but it is your responsibility to evaluate whether each\nadjustment suits your specific needs and environment. Security hardening and\nprocess isolation can introduce stability challenges, compatibility issues, or\nunexpected behavior. Additionally, these protections often come with\nperformance tradeoffs. Always conduct thorough research, there are no plug and\nplay one size fits all security solutions.</p>\n</blockquote>\n<blockquote>\n<p>That said, I typically write about what I’m implementing myself to deepen\nunderstanding and share what works for me. <code>--Source</code> means the proceeding\nparagraph came from <code>--Source</code>, you can often click to check for yourself. If\nyou use some common sense with a bit of caution you could end up with a more\nsecure NixOS system that fits your needs.</p>\n</blockquote>\n<blockquote>\n<p>Much of this guide draws inspiration or recommendations from the well-known\n<a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html\">Linux Hardening Guide</a>\nby Madaidan’s Insecurities. Madaidan’s work is widely regarded in technical\nand security circles as one of the most comprehensive and rigorously\nresearched sources on practical Linux security, frequently cited for its depth\nand actionable advice. For example, much of the original basis for hardening\nfor <a href=\"https://github.com/cynicsketch/nix-mineral\">nix-mineral</a> came from this\nguide as well. This can be a starting point but shouldn’t be blindly followed\neither, always do your own research, things change frequently. Madaidan is\nalso a contributor to both\n<a href=\"https://www.kicksecure.com/wiki/Contributors\">Kicksecure</a> and\n<a href=\"https://www.whonix.org/wiki/Contributors\">Whonix</a>.</p>\n</blockquote>\n<p>For an article with apposing perspectives, see\n<a href=\"https://chyrp.cgps.ch/en/debunking-madaidans-insecurities/\">debunking-madaidans-insecurities</a>.\nWe can learn from both and hopefully find something in between that is closer to\nthe truth.</p>\n<blockquote>\n<p>❗ <strong>Note on SELinux and AppArmor</strong>: While NixOS can provide a high degree of\nsecurity through its immutable and declarative nature, it’s important to\nunderstand the limitations regarding Mandatory Access Control (MAC)\nframeworks. Neither SELinux nor AppArmor are fully supported or widely used in\nthe NixOS ecosystem. You can do a lot to secure NixOS but if anonymity and\nisolation are paramount, I recommend booting into a\n<a href=\"https://tails.net/\">Tails USB stick</a>. Or using\n<a href=\"https://www.whonix.org/\">Whonix</a>.</p>\n</blockquote>\n<p>☝️ The unique file structure of NixOS, particularly the immutable <code>/nix/store</code>,\nmakes it difficult to implement and manage the file-labeling mechanisms that\nthese frameworks rely on. There are ongoing community efforts to improve\nsupport, but as of now, they are considered experimental and not a standard part\nof a typical NixOS configuration. For an immutable distro that implements\nSELinux by default at a system level as well as many other hardening techniques,\nsee <a href=\"https://secureblue.dev/\">Fedora secureblue</a>.</p>\n<p>Containers and VMs are beyond the scope of this chapter but can also enhance\nsecurity and sandboxing if configured correctly. See\n<a href=\"https://saylesss88.github.io/nix/kvm.html\">Running NixOS in a VM</a> for more\ndetails on running NixOS in a Secureblue VM for additional security.</p>\n<p>It’s crucial to <strong>document every change</strong> you make. By creating smaller,\nfeature-complete commits, each with a descriptive message, you’re building a\nclear history. This approach makes it far simpler to revert a breaking change\nand quickly identify what went wrong. Over time, this discipline allows you to\ncreate security-focused checklists and ensure all angles are covered, building a\nmore robust and secure system.</p>\n<p>Don’t rely on single solutions or products, develop processes and defense in\ndepth. Think ahead and fail securely so that a single failure doesn’t mean total\ninsecurity.</p>\n<p>Attackers often monitor the latest Linux CVEs (Common Vulnerabilities and\nExposures) and check if and when specific distributions like NixOS have\nimplemented fixes. The unstable branch will receive the security patches and\nfixes faster than stable which is another thing to keep in mind.</p>\n<p>Check out the\n<a href=\"https://saylesss88.github.io/nix/index.html\">Hardening NixOS Baseline Hardening README</a>\nfor baseline hardening recommendations and best practices.</p>\n<p>There is something to be said about the window manager you use. GNOME, KDE\nPlasma, and Sway secure privileged Wayland protocols like screencopy. This means\nthat on environments outside of GNOME, KDE, and Sway, applications can access\nscreen content of the entire desktop. This implicitly includes the content of\nother applications. It’s primarily for this reason that Silverblue, Kinoite,\nSericea, and COSMIC images are recommended. <del>COSMIC has plans to fix this.</del>\n–<a href=\"https://secureblue.dev/images\">secureblue Images</a></p>\n<blockquote class=\"markdown-alert-important\">\n<p>This is a little misleading, hyprland takes a different approach that also\nworks. Disabling wlroots portal does not block screencopy for all apps, but\nonly with sandboxed clients. Unsandboxed apps (like <code>grim</code>) can still access\next-image-copy-capture directly on Sway without going through the portal.</p>\n</blockquote>\n<ul>\n<li><a href=\"https://wiki.hypr.land/0.50.0/Configuring/Permissions/\">Hyprland Permissions</a></li>\n</ul>\n<p>For example, to disable Xwayland for sway on home-manager you would add:</p>\n<pre><code class=\"language-nix\">wayland.windowManager.sway = {\n  enable = true;\n  extraConfig = ''\n    xwayland disable\n  '';\n}\n</code></pre>\n<ul>\n<li>You may get an error saying you’re only able to disable xwayland at boot,\nrestart your system and you’ll be all set.</li>\n</ul>\n<p>You can explicitly disable <code>xdg-desktop-portal-wlr</code> with systemd in your\n<code>configuration.nix</code> like this:</p>\n<pre><code class=\"language-nix\"># configuration.nix\nsystemd.user.services.\"xdg-desktop-portal-wlr\" = {\n  enable = false;  # Masks/stops the wlr service\n};\nxdg.portal.wlr.enable = false;\n</code></pre>\n<h2>Common Attack Vectors for Linux</h2>\n<details>\n<summary> ✔️ Common Attack Vectors in Linux </summary>\n<p><strong>Privilege escalation</strong>: The unauthorized act of gaining elevated permissions\nrather than legitimate, controlled privilege use. It’s a very common tactic that\nthreat actors use to take over a system, steal data, delete files, and more.</p>\n<p><strong>Processes to protect against Privilege escalation</strong></p>\n<ul>\n<li>\n<p>Adopt the principle of least privilege, only giving users the permissions that\nthey require to perform their duties.</p>\n</li>\n<li>\n<p>Harden your system: Minimize the attack surface, use strong passwords, and\nfollow best practices.</p>\n</li>\n<li>\n<p>Monitor relevant sources such as the\n<a href=\"https://www.strongdm.com/nist-compliance\">NIST National Vulnerability Database</a>,\n<a href=\"https://github.com/NixOS/nix/security/advisories\">NixOS Security Advisories</a>,\nand\n<a href=\"https://discourse.nixos.org/c/announcements/security/56\">NixOS Discourse Security</a>\nSo you’ll know the latest CVEs and vulnerabilities in Linux and NixOS.</p>\n</li>\n<li>\n<p>While not made for NixOS the\n<a href=\"https://github.com/peass-ng/PEASS-ng/tree/master/linPEAS\">linPEAS Privilege Escalation Awesome Script</a>\ngives you some useful info such as active capabilities and potential risks.</p>\n</li>\n<li>\n<p>Remove unnecessary SUID binaries to reduce the attack surface.</p>\n</li>\n</ul>\n<hr />\n<p><strong>Use after Free/Double free</strong>:</p>\n<p><strong>Use-After-Free (UAF)</strong> is a type of software vulnerability that occurs in\nmemory unsafe languages (C C++) when a program continues to use a memory\nlocation after it has been freed or deallocated.</p>\n<p><strong>Double free</strong>: is a flaw where a program frees the same memory block twice\nusing <code>free()</code> or <code>delete</code>, leading to undefined behavior and potential\nexploitation.</p>\n<p>Mitigation techniques include the use of hardened allocators such as\n<code>hardened_malloc</code>, which improve memory management to detect and prevent UAF and\ndouble-free bugs. Recent versions of <code>glibc</code> also incorporate built-in checks to\ncatch double frees.</p>\n<hr />\n<p><strong>Unauthorized Access</strong>:</p>\n<p>Unauthorized access is the entry or use of your system, networks, or data by\nindividuals without permission. It’s a common way for adversaries to exfiltrate\ndata, execute malicious code, and cause damage.</p>\n<p><strong>Protections against Unauthorized Access</strong></p>\n<ul>\n<li>\n<p>Strong Passwords, MFA, and robust Secrets management. In 2025, 22% of breaches\ninvolved stolen credentials overall; in basic web app attacks, 88% used stolen\ncredentials.\n–<a href=\"https://www.strongdm.com/blog/data-breach-statistics\">StrongDM data-breach-statistics</a></p>\n</li>\n<li>\n<p>Close unused ports with a Firewall</p>\n</li>\n<li>\n<p>Encrypt data in transit and at rest</p>\n</li>\n<li>\n<p>Watch your Logs, and deploy intrusion detection systems such as AIDE.</p>\n</li>\n<li>\n<p><a href=\"https://cwe.mitre.org/data/definitions/89.html\">SQL Injection CWE</a>, SQL\ninjection is the most common critical web application vulnerability.</p>\n</li>\n<li>\n<p><a href=\"https://owasp.org/www-community/attacks/xss/\">Cross Site Scripting (XSS)</a></p>\n</li>\n</ul>\n<hr />\n<p><strong>Misconfiguration</strong></p>\n<ul>\n<li>\n<p>With many new users trying NixOS, misconfiguration is common and an easy way\nfor an attacker to gain control over your system.</p>\n</li>\n<li>\n<p>It is recommended to start slowly and try to ensure that you understand your\nconfiguration. Avoid copy-pasting config files that you don’t understand yet.</p>\n</li>\n</ul>\n<hr />\n<p><strong>Zero Day Exploits</strong>:</p>\n<p>The term “Zero-Day” refers to a security vulnerability or flaw that is unknown\nto the software developers or security teams, meaning they have had zero days to\ncreate a patch or fix for it. This term is often associated with concepts such\nas Vulnerabilities, Exploits, and Threats, and it’s important to distinguish\namong them:</p>\n<ul>\n<li>\n<p>A <strong>Zero-Day Vulnerability</strong> is a previously undiscovered security weakness or\nflaw in software that malicious actors can exploit.</p>\n</li>\n<li>\n<p>A <strong>Zero-Day Exploit</strong> describes the specific method or technique attackers\nuse to take advantage of that vulnerability to compromise a system.</p>\n</li>\n<li>\n<p>A <strong>Zero-Day Attack</strong> happens when malicious actors launch an attack using a\nzero-day exploit before the software vendor has had a chance to patch or fix\nthe vulnerability.</p>\n</li>\n<li>\n<p><a href=\"https://docs.google.com/spreadsheets/d/1lkNJ0uQwbeC1ZTRrxdtuPLCIl7mlUreoKfSIgajnSyY/view?gid=0#gid=0\">Project Zero’s 0day spreadsheet</a>.\nYou’ll see that a majority of zero-days are Memory Corruption bugs.</p>\n</li>\n<li>\n<p><a href=\"https://www.zero-day.cz/database/\">Zero-Day tracking project</a></p>\n</li>\n<li>\n<p><a href=\"https://www.zerodayinitiative.com/advisories/published/\"> Trend Micro’s zero day inituative</a></p>\n</li>\n</ul>\n</details>\n<hr />\n<h2>Minimal Installation with LUKS</h2>\n<p>Begin with NixOS’s minimal installation image. This gives you a base system with\nonly essential tools and no extras that could introduce vulnerabilities.</p>\n<p>NixOS’s declarative model makes auditing the installed packages and services\neasy, do so regularly.</p>\n<hr />\n<h2>Manual Encrypted Install Following the Manual</h2>\n<p>Encryption is the process of using an algorithm to scramble plaintext data into\nciphertext, making it unreadable except to a person who has the key to decrypt\nit.</p>\n<p><strong>Data at rest</strong> is data in storage, such as a computer’s or a servers hard\ndisk.</p>\n<p><strong>Data at rest encryption</strong> (typically hard disk encryption), secures the\ndocuments, directories, and files behind an encryption key. Encrypting your data\nat rest prevents data leakage, physical theft, unauthorized access, and more as\nlong as the key management scheme isn’t compromised.</p>\n<ul>\n<li>\n<p><a href=\"https://channels.nixos.org/nixos-25.05/latest-nixos-minimal-x86_64-linux.iso\">Minimal ISO Download (64-bit Intel/AMD)</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/manual/nixos/stable/#sec-installation\">NixOS Manual Installation</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Full_Disk_Encryption\">NixOS Wiki Full Disk Encryption</a></p>\n</li>\n<li>\n<p>The\n<a href=\"https://www.nsa.gov/Press-Room/Press-Releases-Statements/Press-Release-View/Article/3498776/post-quantum-cryptography-cisa-nist-and-nsa-recommend-how-to-prepare-now/\">NSA, CISA, and NIST warn</a>\nthat nation-state actors are likely stockpiling encrypted data now, preparing\nfor a future when quantum computers could break today’s most widely used\nencryption algorithms. Sensitive data with long-term secrecy needs is\nespecially at risk.</p>\n</li>\n<li>\n<p><a href=\"https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/3630145/cybersecurity-speaker-series-preparing-for-post-quantum/\">NSA/CSS Preparing for Post-Quantum</a></p>\n</li>\n<li>\n<p>This is a wake-up call to use the strongest encryption available today and to\nplan early for post-quantum security.</p>\n</li>\n<li>\n<p><a href=\"https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards\">NIST First 3 Post-Quantum Encryption Standards</a>\nOrganizations and individuals should prepare to migrate cryptographic systems\nto these new standards as soon as practical.</p>\n</li>\n<li>\n<p>They chose\n<a href=\"https://www.nist.gov/news-events/news/2022/07/nist-announces-first-four-quantum-resistant-cryptographic-algorithms\">Four Quantum-Resistant Cryptographic Algorithms</a>\nwarning that public-key cryptography is especially vulnerable and widely used\nto protect digital information.</p>\n</li>\n</ul>\n<hr />\n<h2>Guided Encrypted BTRFS Subvol install using disko</h2>\n<p>Use LUKS encryption to protect your data at rest, the following guide is a\nminimal disko encrypted installation:\n<a href=\"https://saylesss88.github.io/installation/enc/enc_install.html\">Encrypted Install</a></p>\n<hr />\n<h2>Installing Software</h2>\n<ul>\n<li>\n<p><a href=\"https://determinate.systems/blog/nixpkgs-cooldown/\">Nixpkgs cooldowns</a>, this\nis a great addition after all of the AUR compromises. “<a href=\"https://flakehub.com/flake/DeterminateSystems/nixpkgs-weekly?view=usage\">nixpkgs-weekly</a> still updates once a\nweek, but deliberately introduces a seven-day “cooldown” period, updating to a\nnew revision of Nixpkgs released by upstream only after seven days have elapsed.\nThis introduces a buffer period where major vulnerabilities or attacks can be\nidentified prior to it arriving in our users’ hands.“</p>\n</li>\n<li>\n<p>Checkout <a href=\"https://cooldowns.dev/\">cooldowns.dev</a> for more info on cooldowns.</p>\n</li>\n</ul>\n<blockquote>\n<p>The 2025 Edgescan study examined full-stack applications and found that\none-third contained critical or severe vulnerabilities, putting them at risk.\nOver 45% of large enterprises leave unresolved vulnerabilities for more than a\nyear. This shows the necessity of containing your apps in sandboxes when\npossible.\n–<a href=\"https://www.edgescan.com/stats-report/\">edgescan Vulnerability Report</a></p>\n</blockquote>\n<blockquote class=\"markdown-alert-caution\">\n<p>⚠️ For system security it is strongly advised to not install\n<a href=\"https://en.wikipedia.org/wiki/Proprietary_software\">proprietary</a>,\n<a href=\"https://www.gnu.org/proprietary/proprietary.html\">non-freedom</a> software.\nInstead, use of\n<a href=\"https://www.fsf.org/about/what-is-free-software\">Free Software</a> is\n<a href=\"https://www.gnu.org/philosophy/shouldbefree.html\">recommended</a> –Kicksecure</p>\n</blockquote>\n<ul>\n<li><a href=\"https://www.gnu.org/proprietary/proprietary.html\">Proprietary Software is Often Malware</a>\nNOTE: While I respect the importance of software freedom, I choose to focus on\npractical, technical solutions rather than engage with the ideological tone\noften present in related advocacy.\n<ul>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Miscellaneous_Threats_to_User_Freedom\">User Freedom Threats</a></p>\n</li>\n<li>\n<p><a href=\"https://www.gnu.org/proprietary/proprietary-back-doors.html\">Proprietary Back Doors</a></p>\n</li>\n<li>\n<p><a href=\"https://www.eff.org/deeplinks/2015/02/who-really-owns-your-drones\">EFF Back Doors</a></p>\n</li>\n</ul>\n</li>\n</ul>\n<pre><code class=\"language-nix\"># configuration.nix\nnixpkgs.config.allowUnfree = false;\n</code></pre>\n<p>To explicitly disable it for flakes:</p>\n<pre><code class=\"language-nix\"># ...snip...\npkgs = import nixpkgs {\n  system = \"x86_64-linux\";\n  config = {\n    allowUnfree = false;\n  };\n};\n# ...snip...\n</code></pre>\n<p>Most users don’t fully understand that running any software without sandboxing\ngives it unrestricted access to their user data and system resources. There is a\nwidespread lack of awareness that Linux apps generally run with the full\npermissions of the user. It’s easy to overlook the fact that “trusted source”\ndoesn’t mean “safe to run uncontained”. –summarized from kicksecure docs</p>\n<p><strong>Pre-Install Recommendations</strong></p>\n<ul>\n<li><a href=\"https://nixos.org/community/teams/security/\">NixOS Security</a></li>\n</ul>\n<p>When installing software, first check\n<a href=\"https://search.nixos.org/packages\">search.nixos</a>, and follow the <code>Homepage</code>\nlink to ensure that said package is maintained.</p>\n<p>For example, when I search for <code>doas</code>, and go to the\n<a href=\"https://github.com/Duncaen/OpenDoas\">Homepage</a> link, I can see that the most\nrecent commit was made 3 years ago. For certain software this might not be an\nissue but <code>doas</code> isn’t one of them.</p>\n<p>Looking at the <code>sudo-rs</code>\n<a href=\"https://github.com/trifectatechfoundation/sudo-rs\">Homepage</a> I can see that it\nwas updated yesterday (11-19-25) and might be a better alternative. It’s\nmaintained and written in a memory safe language.</p>\n<p>For critical apps like <code>sudo</code>, you should also check for vulnerabilities in said\nsoftware. If you did so for <code>sudo-rs</code>, you’d see\n<a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-64170\">CVE-2025-64170</a> and see that\nit’s been patched. You can then look at the\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/su/sudo-rs/package.nix\">sudo-rs package.nix</a>\nto ensure that the versions match. (As of 11-20-25 they match).</p>\n<hr />\n<p><strong>nixpkgs-unstable Security Overview</strong></p>\n<ul>\n<li>\n<p><code>nixpkgs-unstable</code> tracks the master branch of the Nixpkgs repo and is\nconstantly updated.</p>\n</li>\n<li>\n<p>This branch gets security updates faster, patching vulnerabilities faster.</p>\n</li>\n<li>\n<p>Since it’s a rolling-release, packages are less thoroughly tested. This\nincreases the risk of new, undiscovered bugs or regressions. Some of which\ncould have security implications.</p>\n</li>\n<li>\n<p>The packages are generally the most recent upstream versions, which is\nimportant for security-sensitive software like browsers and kernels, as old\nversions may have publicly known, unpatched vulnerabilities.</p>\n</li>\n<li>\n<p>As the name states, <code>nixpkgs-unstable</code> is less stable and an update is more\nlikely to cause your system to fail to build due to breaking changes in Nix\nexpressions.</p>\n</li>\n<li>\n<p>I personally use unstable for everything, but I don’t mind having to fix\nissues that arise.</p>\n</li>\n</ul>\n<hr />\n<p><strong>Stable (e.g., <code>nixos-24.05</code>) Security Overview</strong></p>\n<p>Stable Nixpkgs channels correspond to point release (e.g., released every 6\nmonths) and are supported for a limited period (typically one month past the\nnext release).</p>\n<ul>\n<li>\n<p>Stable channels generally only receive conservative bug and security fixes.\nMajor version bumps for features are typically avoided to maintain “stability\nagainst deliberate changes”, which means you won’t get the latest upstream\nfeatures or general bug fixes.</p>\n</li>\n<li>\n<p>While critical security updates are backported quickly, updates for less\ncritical packages may be slower or not happen at all if they require a\nsignificant refactoring or version bump.</p>\n</li>\n<li>\n<p>Stable channels are generally more stable, meaning updates are less likely to\nintroduce breaking changes to your configuration or system environment.</p>\n</li>\n<li>\n<p>Many packages will be older versions. If a critical security vulnerability\nrequires a major upstream version update (which is often avoided in a stable\nchannel), the maintainers must backport the patch, a process which can\nintroduce its own set of risks and delays.</p>\n</li>\n</ul>\n<hr />\n<p><strong>What should you use?</strong></p>\n<p>The primary security trade-off is between <strong>patching speed for known\nvulnerabilities</strong> and <strong>stability/exposure to new bugs</strong>:</p>\n<ul>\n<li>\n<p>Choose <code>unstable</code> if you prioritize getting the latest security fixes\n(especially for end-user apps like browsers) as soon as they are available\nupstream, accepting a higher risk of non-security-related system breakage or\nnew, undiscovered bugs.</p>\n</li>\n<li>\n<p>Choose <code>stable</code> if you prioritize system predictability and stability, relying\non dedicated backports for critical vulnerabilities, while accepting that\nnon-critical security and bug fixes will be delayed or absent until the next\nmajor release.</p>\n</li>\n</ul>\n<p>A common hybrid approach is to use the <code>stable</code> channel as the base for the OS\nand selectively pin specific packages from <code>unstable</code> to ensure they receive\nrapid security updates.</p>\n<p>With flakes it’s easy to add both <code>stable</code> and <code>unstable</code> as flake inputs and\naccess each with some simple logic.</p>\n<details>\n<summary> ✔️ Click to Expand Flake example using both stable & unstable </summary>\n<pre><code class=\"language-nix\">{\n  description = \"NixOS configuration with two or more channels\";\n\n inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-25.05\";\n    nixpkgs-unstable.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n  };\n\n  outputs =\n    { nixpkgs, nixpkgs-unstable, ... }:\n    {\n      nixosConfigurations.\"your-host\" = nixpkgs.lib.nixosSystem {\n        modules = [\n          {\n            nixpkgs.overlays = [\n              (final: prev: {\n                unstable = nixpkgs-unstable.legacyPackages.${prev.system};\n                # use this variant if unfree packages are needed:\n                # unstable = import nixpkgs-unstable {\n                #   inherit prev;\n                #   system = prev.system;\n                #   config.allowUnfree = true;\n                # };\n              })\n            ];\n          }\n          ./configuration.nix\n        ];\n      };\n    };\n}\n</code></pre>\n<ul>\n<li>This is also how you enable unfree packages for flakes rather than in your\n<code>configuration.nix</code>.</li>\n</ul>\n<p>Now you can specify which packages are to be installed with which channel like\nso:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{ pkgs, ... }:\n{\n  environment.systemPackages = [\n    pkgs.firefox\n    pkgs.unstable.helix\n  ];\n  # ...\n}\n</code></pre>\n</details>\n<hr />\n<h2>Users and SUID Binaries</h2>\n<p><strong>Replacing sudo with run0</strong></p>\n<blockquote class=\"markdown-alert-note\">\n<p>The point here is to avoid using the setuid binary (<code>sudo</code>), <code>run0</code> is a\nwrapper over <code>systemd-run</code> which speaks over Inter-process Communication\nMechanisms (IPC) to PID1 which is considered safer than running a setuid\nbinary. We separate our daily user from administration tasks and authenticate\nthrough our admin account. This reduces the attack surface by removing sudo as\nwell as reduces the risk of local privilege escalation.</p>\n</blockquote>\n<ul>\n<li>\n<p><strong>IPC</strong> is the mechanism that allows processes to communicate. There are two\nmethods of IPC, shared memory and message passing. An OS can implement both.</p>\n</li>\n<li>\n<p><strong>PID 1</strong> is the first userspace process the kernel starts (the init system),\nwhich becomes the ancestor and reaper of all other processes; because it runs\nas root, is always present, and controls the system lifecycle, any bugs or\ndesign issues in PID 1 have outsized security impact and can translate into\nsystem-wide compromise or denial of service.</p>\n</li>\n</ul>\n<details>\n<summary> Click to Expand SUID and run0 resources </summary>\n<ul>\n<li>\n<p><a href=\"https://mastodon.social/@pid_eins/112353324518585654\">run0 explained by Lennart</a></p>\n</li>\n<li>\n<p><a href=\"https://en.wikipedia.org/wiki/Setuid\">setuid Wikipedia</a></p>\n</li>\n<li>\n<p>Using <code>run0</code> removes of these classes of\n<a href=\"https://ruderich.org/simon/notes/su-sudo-from-root-tty-hijacking\">attacks</a></p>\n</li>\n<li>\n<p>The following lists some of the downsides\n<a href=\"https://www.kicksecure.com/wiki/Dev/secureblue\">kicksecure vs secureblue</a></p>\n</li>\n</ul>\n</details>\n<p><code>run0</code> is not a SUID, it asks the service manager to invoke a command or shell\nunder the target user’s UID. The target command is invoked in an isolated exec\ncontext, freshly forked off PID1 without inheriting any context from the client.</p>\n<p>The core danger of <strong>setuid</strong> (Set User ID) lies in its ability to allow a\nlow-privilege user to execute a program with the <strong>permissions of the file’s\nowner</strong>, which is most often the powerful <strong>root user</strong>.</p>\n<h3>💥 The Danger of setuid</h3>\n<p>For granting limited, controlled privilege escalation to apps, the primary\nchoices are broadly between traditional <strong>setuid/setgid permissions</strong> and more\nmodern <strong>Linux capabilities</strong>. <a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html#capabilities\">Jump to Capabilities</a></p>\n<ul>\n<li><a href=\"https://www.cbtnuggets.com/blog/technology/system-admin/linux-file-permissions-understanding-setuid-setgid-and-the-sticky-bit\">Understanding setuid/setgid</a></li>\n</ul>\n<p>Use the following command to find all SUID binaries:</p>\n<pre><code class=\"language-bash\">sudo find / -perm -4000 -type f -ls 2&gt;/dev/null\n</code></pre>\n<p>The <code>setuid</code> permission is dangerous because it creates a privilege escalation\npathway that can be exploited for malicious purposes.</p>\n<ul>\n<li>\n<p>Temporary Root Access: When a file has the setuid bit set and is owned by\n<code>root</code>, any user who executes that program instantly and temporarily gains the\nfull power of the root user while the program runs.</p>\n</li>\n<li>\n<p>If a setuid program (such as <code>passwd</code>, or <code>sudo</code>) contain a security flaw,\nsuch as a buffer overflow (Common in C) or improper input validation, an\nattacker can exploit the flaw.</p>\n</li>\n<li>\n<p>Since the program is running with root privileges, the attacker can execute\nshell code or commands with root access, completely compromising the entire\nsystem.</p>\n</li>\n</ul>\n<p>Normally the root user (UID 0) gets unrestricted access to almost everything on\nthe entire system.</p>\n<p>I rebuild/update way too often to completely separate the accounts and allow no\nadmin tasks for my daily user. That may be a better option for servers, etc.</p>\n<p>Create an admin user for administrative tasks and remove your daily user from\nthe <code>wheel</code> group, and disable the <code>sudo</code>, <code>su</code>, and <code>pkexec</code> SUIDs:</p>\n<p>(Edited: 2026-02-01): Changed from disabling the <code>setuid</code> bits to disabling the\nwrapper entirely. Caught by <code>SuperSandro2000</code></p>\n<pre><code class=\"language-nix\">{ config, pkgs, lib }:\n{\nusers.users.admin = {\n    isNormalUser = true;\n    description  = \"System administrator\";\n    extraGroups  = [ \"wheel\" ];   # wheel = sudo\n    # run `mkpasswd --method=yescrypt` and replace \"changeme\" w/ the result\n    initialHashedPassword = \"changeme\";           # change with `passwd admin` later\n    openssh.authorizedKeys.keys = [\n      # (optional) paste your SSH public key here\n      # \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...\"\n    ];\n  };\n    users.groups.admin = {};\n    users.mutableUsers = false;\n\n  # --------------------------------------------------------------------\n  # 2. Existing daily user – remove from wheel, keep everything else\n  # --------------------------------------------------------------------\n  users.users.daily = {\n    isNormalUser = true;\n    description  = \"Daily driver account\";\n    extraGroups  = lib.mkForce [ \"networkmanager\" \"audio\" \"video\" ]; # keep useful groups\n    initialHashedPassword = \"changeme\";\n    # Remove `wheel` by *not* listing it (mkForce overrides any default)\n  };\n  users.groups.daily = {};\n\nsecurity = {\n# alias sudo = 'run0'\nrun0.enableSudoAlias = true;\npolkit.enable = true;\n# Disable sudo\nsudo.enable = false;\nwrappers = {\n    su.enable = lib.mkForce false;\n    sudoedit.enable = lib.mkForce false;\n    sg.enable = lib.mkForce false;\n    fusermount.enable = lib.mkForce false;\n    fusermount3.enable = lib.mkForce false;\n    pkexec.setuid = lib.mkForce false;\n    newgrp.setuid = lib.mkForce false;\n    newgidmap.setuid = lib.mkForce false;\n    newuidmap.setuid = lib.mkForce false;\n    # `mount` Needed for `fileSystems.options`\n    # mount.enable = lib.mkForce false;\n    # Optional: if you disable mount, disable umount as well\n    # umount.enable = lib.mkForce false;\n};\n# Or hyprlock, required for swaylock to accept your password\npam.services.swaylock = {\n  text = ''\n    auth include login\n    account include login\n    password include login\n    session include login\n  '';\n  };\n};\n</code></pre>\n<p>The <code>security.wrappers...</code> removes the setuid bit making the commands unusable\nremoving the SUID vulnerabilities for <code>su</code> and <code>pkexec</code>. You can find the other\nSUID wrappers in <code>/run/wrappers/bin/</code>, such as <code>fusermount</code> and more.</p>\n<p>SUID’s that can be disabled:</p>\n<ul>\n<li>\n<p><code>umount</code>: Allows unprivileged users to unmount devices listed in your fstab.</p>\n</li>\n<li>\n<p><code>mount</code>: Same as above but for mounting. It is recommended to set\n<code>fileSystems.\"/boot\".options = [ \"fmask=0077\" \"dmask=0077\" ];</code> this won’t work\nwithout <code>mount</code>s setuid.</p>\n</li>\n<li>\n<p><code>sg</code>: Executes a command as a different group.</p>\n</li>\n<li>\n<p><code>mtr-packet</code>: Used by mtr to create network sockets.</p>\n</li>\n<li>\n<p><code>fusermount</code>, <code>fusermount3</code>: Allows unprivileged users to mount FUSE\nfilesystems. Can be disabled if you don’t use FUSE (e.g., Appimages, etc.)</p>\n</li>\n<li>\n<p><code>newuidmap</code>, <code>newgidmap</code>: Used for user namespace creation (Often used for\nunprivileged containers). (Disable if you don’t use unprivileged\ncontainers/namespaces)</p>\n</li>\n</ul>\n<hr />\n<p>Never Disable:</p>\n<ul>\n<li><code>unix_chkpwd</code>: This is a core PAM helper to securely check user passwords\nagainst the root-readable <code>/etc/shadow</code>.</li>\n</ul>\n<p>Check again which SUID binaries are active:</p>\n<pre><code class=\"language-bash\">sudo find / -perm -4000 -type f -ls 2&gt;/dev/null\n# Example\n-rwsr-xr-x  root  root  /run/wrappers/bin/fusermount\n   ^-- This 's' means setuid bit is set\n</code></pre>\n<pre><code class=\"language-bash\">ls -la /run/wrappers/bin/\n# Or\nfind /run/wrappers -perm -4000 -ls\n</code></pre>\n<p><strong>Only enable the wrappers you actually use!</strong></p>\n<hr />\n<p>You will have to use <code>run0</code> to authenticate your daily user, for example:</p>\n<pre><code class=\"language-bash\">run0 nixos-rebuild switch --flake .\n</code></pre>\n<p>Since <code>run0</code> doesn’t cache results and <code>nixos-rebuild</code> calls on Polkit 3 times,\nso on every rebuild, you will be asked for your password 3 times which isn’t\nideal. I found the following workaround that will only ask for your password\nonce.</p>\n<p>Add the following to your <code>configuration.nix</code>, replacing <code>user-name</code> with your\nusername:</p>\n<pre><code class=\"language-nix\"> security.polkit.extraConfig = ''\n     polkit.addRule(function(action, subject) {\n       if (subject.user == \"user-name\") {\n         if (action.id.indexOf(\"org.nixos\") == 0) {\n           polkit.log(\"Caching admin authentication for single NixOS operation\");\n           return polkit.Result.AUTH_ADMIN_KEEP;\n         }\n       }\n     });\n   '';\n</code></pre>\n<p>Create a zsh function for easy access:</p>\n<pre><code class=\"language-nix\"># zsh.nix\n#...snip...\ninitContent = ''\n  fr() {\n    run0 nixos-rebuild switch --flake \"/home/$USER/flake#\"$(hostname)\n  }\n'';\n</code></pre>\n<p>Needless to say, this is less secure but much more convenient than entering your\npassword 3 times on every single rebuild.</p>\n<p>Without the <code>pam</code> settings for swaylock, it won’t accept your password to log\nback in.</p>\n<p><strong>run0 Usage Example</strong></p>\n<p>When you are in a privileged shell, <code>run0</code> changes the color of the background\nto red to remind you of this.</p>\n<p>Example creating a user:</p>\n<ol>\n<li>\n<p><code>run0</code></p>\n</li>\n<li>\n<p><code>adduser admin</code></p>\n</li>\n<li>\n<p><code>usermod -aG wheel admin</code></p>\n</li>\n<li>\n<p><code>passwd admin</code></p>\n</li>\n<li>\n<p><code>exit</code></p>\n</li>\n<li>\n<p><code>reboot</code></p>\n</li>\n</ol>\n<p>This is just an example, since we manage our users declaratively the user\ncreated would be discarded on the next rebuild because of the\n<code>users.mutableUsers = false;</code> setting. You could of course change this to <code>true</code>\nto manage your users imperatively but I don’t recommend it.</p>\n<hr />\n<h3>Capabilities</h3>\n<details>\n<summary> ✔️ Click to expand capabilities examples </summary>\n<p>One way to help get rid of setuid binaries is to replace them with capabilities.\nI personally only remove the SUID bit and don’t try to replace with capabilities\nas of now. You can still use the commands from <code>security.wrappers</code> such as\n<code>run0 su -</code>.</p>\n<p>Capabilities provide a subset of what is available to root to a process. This\nbreaks up root privileges into smaller units that can independently grant access\nto processes. This reduces the full set of privileges, decreasing the risk of\nexploitation.</p>\n<p>(This is just an example):</p>\n<pre><code class=\"language-nix\">{\n  # a setuid root program\n  doas =\n    { setuid = true;\n      owner = \"root\";\n      group = \"root\";\n      source = \"${pkgs.doas}/bin/doas\";\n    };\n\n  # a setgid program\n  locate =\n    { setgid = true;\n      owner = \"root\";\n      group = \"mlocate\";\n      source = \"${pkgs.locate}/bin/locate\";\n    };\n\n  # a program with the CAP_NET_RAW capability\n  ping =\n    { owner = \"root\";\n      group = \"root\";\n      capabilities = \"cap_net_raw+ep\";\n      source = \"${pkgs.iputils.out}/bin/ping\";\n    };\n}\n</code></pre>\n<p>List the highest capability number for your kernel with:</p>\n<pre><code class=\"language-bash\">cat /proc/sys/kernel/cap_last_cap\n# Output:\n40\n</code></pre>\n<p>List available Linux capabilities:</p>\n<pre><code class=\"language-bash\">capsh --print\n</code></pre>\n<p>List processes:</p>\n<pre><code class=\"language-bash\">ps\n# Example Output\nPID    TTY     TIME   CMD\n8063   pts/1    02     zsh\n</code></pre>\n<pre><code class=\"language-bash\">cat /proc/8063/status | grep Cap\n# Output\nCapInh: 0000000800000000\nCapPrm: 0000000000000000\nCapEff: 0000000000000000\nCapBnd: 000001ffffffffff\nCapAmb: 0000000000000000\n</code></pre>\n<pre><code class=\"language-bash\">capsh --decode=000001ffffffffff\n# Output\n0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore\n</code></pre>\n<p><code>cap_net_raw</code>: Allows the program to use raw and unbuffered network sockets,\nwhich is what <code>ping</code> and <code>mtr-packet</code> need to send ICMP packets.</p>\n<p><code>cap_sys_admin</code>: Grants a variety of system administration operations, including\nthe ability to perform FUSE mounts. This is a powerful capability, but it’s\nstill more restrictive than full root SUID.</p>\n<ul>\n<li><code>+ep</code>: This is crucial. It stands for:\n<ul>\n<li>\n<p><code>e</code> (Effective): The set of capabilities actually used by the process when\nrunning.</p>\n</li>\n<li>\n<p><code>p</code> (Permitted): The set of capabilities that can be enabled by the process.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>By using this approach, you are following the security principle of least\nprivilege, significantly reducing the attack surface compared to traditional\nSUID binaries.</p>\n<ul>\n<li>\n<p><a href=\"https://search.nixos.org/options?channel=unstable&amp;show=security.wrappers&amp;query=security.wrappers\">security.wrappers</a></p>\n</li>\n<li>\n<p><a href=\"https://linux-audit.com/kernel/capabilities/linux-capabilities-101/\">Linux Audit capabilities 101</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Dev/secureblue#capabilities\">Kicksecure’s take on capabilities</a></p>\n</li>\n<li>\n<p><a href=\"https://man7.org/linux/man-pages/man7/capabilities.7.html\">capabilities(7)</a></p>\n</li>\n<li>\n<p><a href=\"https://docs.redhat.com/en/documentation/red_hat_enterprise_linux_atomic_host/7/html/container_security_guide/linux_capabilities_and_seccomp\">capabilities and seccomp</a></p>\n</li>\n</ul>\n</details>\n<hr />\n<h2>Impermanence</h2>\n<p>Impermanence, especially when using a <code>tmpfs</code> as the root filesystem, provides\nseveral significant security benefits. The core principle is that impermanence\ndefeats persistence, a fundamental goal for any attacker.</p>\n<p>When you use a root-as-tmpfs setup on NixOS, the boot process loads the entire\noperating system from the read-only Nix store into a <code>tmpfs</code> in RAM. The mutable\ndirectories, such as <code>/etc</code> and <code>/var</code>, are then created on this RAM disk. When\nthe system is shut down, the <code>tmpfs</code> is wiped, leaving the on-disk storage\nuntouched and secure.</p>\n<p>This means you get a fresh, secure boot every time, making it much harder for an\nattacker to maintain a presence on your system.</p>\n<ul>\n<li>\n<p><a href=\"https://grahamc.com/blog/erase-your-darlings/\">Erase your Darlings (ZFS)</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/installation/enc/encrypted_impermanence.html\">Encrypted BTRFS Impermanence Guide</a>\nOnly follow this guide if you also followed the encrypted disko install,\nimpermanence is designed to be destructive and needs to match your config\nexactly.</p>\n</li>\n</ul>\n<h2>Replace timesyncd with a chron job that enables Network Time Security (NTS)</h2>\n<p>This is implementing the GrapheneOS/secureblue NTS chrony settings to NixOS:</p>\n<pre><code class=\"language-nix\">{ config\n, ...\n}:\n{\n  services.chrony = {\n    enable = true;\n    enableNTS = true;\n    servers = [\n        \"server time.cloudflare.com iburst nts\"\n        \"server ntppool1.time.nl iburst nts\"\n        \"server nts.netnod.se iburst nts\"\n        \"server ptbtime1.ptb.de iburst nts\"\n        \"server time.dfm.dk iburst nts\"\n        \"server time.cifelli.xyz iburst nts\"\n     ];\n    # havent worked out the kinks yet\n  #  extraConfig = ''\n  #      minsources 3\n  #      authselectmode require\n\n  #      # EF\n  #      dscp 46\n\n  #      driftfile /var/lib/chrony/drift\n  #      dumpdir /var/lib/chrony\n  #      ntsdumpdir /var/lib/chrony\n\n  #      leapseclist /usr/share/zoneinfo/leap-seconds.list\n  #      makestep 1.0 3\n\n  #      rtconutc\n\n  #      cmdport 0\n\n  #      noclientlog\n  #  '';\n  };\n}\n</code></pre>\n<p>Ensure NTS is being used with:</p>\n<pre><code class=\"language-bash\">sudo chronyc -N authdata\n</code></pre>\n<hr />\n<h2>Secure Boot</h2>\n<!-- ![Virus](../images/virus1.png) -->\n<p>Enable a UEFI password or Administrator password where it requires\nauthentication in order to access the UEFI/BIOS.</p>\n<p>Secure Boot helps ensure only signed, trusted kernels and bootloaders are\nexecuted at startup.</p>\n<p>Useful Resources:</p>\n<details>\n<summary> ✔️ Click to Expand Secure Boot Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://0pointer.net/blog/authenticated-boot-and-disk-encryption-on-linux.html\">The Strange State of Authenticated Boot and Encryption</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Secure_Boot\">NixOS Wiki Secure Boot</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nix-community/lanzaboote\">lanzaboote repo</a></p>\n</li>\n</ul>\n</details>\n<p>Practical Lanzaboote Secure Boot setup for NixOS:\n<a href=\"https://saylesss88.github.io/installation/enc/lanzaboote.html\">Guide:Secure Boot on NixOS with Lanzaboote</a></p>\n<hr />\n<h3>The Kernel</h3>\n<p>The Kernel Self Protection Project:</p>\n<ul>\n<li><a href=\"https://kspp.github.io/Recommended_Settings\">KSPP Recommended_Settings</a></li>\n</ul>\n<p>Given the kernel’s central role, it’s a frequent target for malicious actors,\nmaking robust hardening essential.</p>\n<p>NixOS provides a <code>hardened</code> profile that applies a set of security-focused\nkernel and system configurations.</p>\n<p>For flakes, you could do something like the following in your\n<code>configuration.nix</code> or equivalent to import <code>hardened.nix</code> and enable\n<code>profiles.hardened</code>:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{ pkgs, inputs, ... }: let\n   modulesPath = \"${inputs.nixpkgs}/nixos/modules\";\n\nin {\n  imports = [ \"${modulesPath}/profiles/hardened.nix\" ];\n\n}\n</code></pre>\n<ul>\n<li>\n<p>There is a proposal to remove it completely that has gained ground, the\nfollowing thread discusses why:\n<a href=\"https://discourse.nixos.org/t/proposal-to-deprecate-the-hardened-profile/63081\">Discourse Thread</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/NixOS/nixpkgs/pull/383438\">PR #383438</a> Proposed removal\nPR.</p>\n</li>\n<li>\n<p>Check\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix\">hardened.nix</a>\nto see exactly what adding it enables to avoid duplicates and conflicts moving\non. I included this for completeness, the choice is yours if you want to use\nit or not.</p>\n</li>\n</ul>\n<h2>Choosing your Kernel</h2>\n<p>See which kernel you’re currently using with:</p>\n<pre><code class=\"language-bash\"># show the kernel release\nuname -r\n# show kernel version, hostname, and architecture\nuname -a\n</code></pre>\n<p>Show the configuration of your current kernel:</p>\n<pre><code class=\"language-bash\">zcat /proc/config.gz\n# ...snip...\n#\n# Compression\n#\nCONFIG_CRYPTO_DEFLATE=m\nCONFIG_CRYPTO_LZO=y\nCONFIG_CRYPTO_842=m\nCONFIG_CRYPTO_LZ4=m\nCONFIG_CRYPTO_LZ4HC=m\nCONFIG_CRYPTO_ZSTD=y\n# end of Compression\n# ...snip...\n</code></pre>\n<p>The <a href=\"https://nixos.org/manual/nixos/stable/#sec-kernel-config\">NixOS Manual</a>\nstates that the default Linux kernel configuration should be fine for most\nusers.</p>\n<p>The Linux kernel is typically released under two forms: stable and long-term\nsupport (LTS). Choosing either has consequences, do your research.\n<a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html#stable-vs-lts\">Stable vs. LTS kernels</a></p>\n<ul>\n<li><a href=\"https://www.kernel.org/category/releases.html\">The Linux Kernel Archives Active kernel releases</a></li>\n</ul>\n<p><strong>OR</strong>, you can choose the hardened kernel for a kernel that prioritizes\nsecurity over everything else.</p>\n<hr />\n<h3>The Hardened Kernel</h3>\n<blockquote class=\"markdown-alert-note\">\n<p>Expect breakage when using the hardened kernel. <code>linux-hardened</code> completely\ndisables\n<a href=\"https://secureblue.dev/articles/userns\">unprivileged user namespaces</a>, which\nare required for Flatpak, chromium-based browsers, and more.</p>\n</blockquote>\n<p>The <code>linuxPackages_latest_hardened</code> attribute has been deprecated. If you want\nto use a hardened kernel, it is now recommended to use <code>linux_hardened</code>, which\nis aliased to <code>linux_default.kernel</code>.</p>\n<p>You can find the latest available hardened kernel packages by searching\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/linux-kernels.nix\">pkgs/top-level/linux-kernels.nix</a>.</p>\n<p>It is recommended to use <code>linux_hardened</code> without specifying a version, such as:</p>\n<pre><code class=\"language-nix\">boot.kernelPackages = pkgs.linuxPackages_hardened;\n</code></pre>\n<p><code>linux_hardened</code> is aliased to the <code>linux_default.kernel</code>.</p>\n<p>Note that this not only replaces the kernel, but also packages that are specific\nto the kernel version, such as NVIDIA video drivers. This also removes your\nability to use the <code>.extend</code> kernel attribute, they are only available to\n<em>kernel package sets</em> (e.g., <code>linuxPackages_hardened</code>)</p>\n<ul>\n<li>If you decide to use this, read further before rebuilding.</li>\n</ul>\n<p>You can inspect\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/kernel/hardened/patches.json\">nixpkgs/pkgs/os-specific/linux/kernel/hardened/patches.json</a>\nto see the metadata of the patches that are applied. You can then follow the\nlinks in the <code>.json</code> file to see the patch diffs.</p>\n<hr />\n<h3>sysctl</h3>\n<p>A tool for checking the security hardening options of the Linux kernel:</p>\n<pre><code class=\"language-nix\">environment.systemPackages = [ pkgs.kernel-hardening-checker ];\n</code></pre>\n<p><code>sysctl</code> is a tool that allows you to view or modify kernel settings and\nenable/disable different features.</p>\n<p>Check all <code>sysctl</code> parameters against the <code>kernel-hardening-checker</code>\nrecommendations:</p>\n<pre><code class=\"language-bash\">sudo sysctl -a &gt; params.txt\nkernel-hardening-checker -l /proc/cmdline -c /proc/config.gz -s ./params.txt\n</code></pre>\n<p>Check the value of a specific parameter:</p>\n<pre><code class=\"language-bash\">sudo sysctl -a | grep \"kernel.kptr_restrict\"\n# Output:\nkernel.kptr_restrict = 2\n</code></pre>\n<p>Check Active Linux Security Modules:</p>\n<pre><code class=\"language-bash\">cat /sys/kernel/security/lsm\n# Output:\nFile: /sys/kernel/security/lsm\ncapability,landlock,yama,bpf,apparmor\n</code></pre>\n<p>Check Kernel Configuration Options:</p>\n<pre><code class=\"language-bash\">zcat /proc/config.gz | grep CONFIG_SECURITY_SELINUX\nzcat /proc/config.gz | grep CONFIG_HARDENED_USERCOPY\nzcat /proc/config.gz | grep CONFIG_STACKPROTECTOR\n</code></pre>\n<p>Since it is difficult to see exactly what enabling the hardened_kernel does.\nBefore rebuilding, you could do something like this to see exactly what is\nadded:</p>\n<pre><code class=\"language-bash\">sudo sysctl -a &gt; before.txt\n</code></pre>\n<p>And after the rebuild:</p>\n<pre><code class=\"language-bash\">sudo sysctl -a &gt; after.txt\n</code></pre>\n<p>And finally run a <code>diff</code> on them:</p>\n<pre><code class=\"language-bash\">diff before.txt after.txt\n</code></pre>\n<p>You can also diff against <code>after.txt</code> for future changes to avoid duplicates,\nthis seems easier to me than trying to parse through the patches.</p>\n<hr />\n<h2>Kernel Security Settings</h2>\n<pre><code class=\"language-nix\">security = {\n      protectKernelImage = true;\n      lockKernelModules = false; # this breaks iptables, wireguard, and virtd\n\n      # force-enable the Page Table Isolation (PTI) Linux kernel feature\n      forcePageTableIsolation = true;\n\n      # User namespaces are required for sandboxing.\n      # this means you cannot set `\"user.max_user_namespaces\" = 0;` in sysctl\n      allowUserNamespaces = true;\n\n      # Disable unprivileged user namespaces, unless containers are enabled\n      unprivilegedUsernsClone = config.virtualisation.containers.enable;\n      allowSimultaneousMultithreading = true;\n}\n</code></pre>\n<hr />\n<h2>Further Hardening with sysctl</h2>\n<p><code>sysctl</code> hardening settings further reinforce kernel-level protections. The\nhardened kernel includes security patches and stricter defaults, but it doesn’t\ncover all runtime tunables. Refer to the above commands to get a diff of the\nchanges.</p>\n<p><a href=\"https://nixos.org/manual/nixos/stable/options#opt-boot.kernel.sysctl\">boot.kernel.sysctl</a>:\nRuntime parameters of the Linux kernel, as set by sysctl(8). Note that the\nsysctl parameters names must be enclosed in quotes. Values may be a string,\ninteger, boolean, or null.</p>\n<p>Check what each setting does <a href=\"https://sysctl-explorer.net/\">sysctl-explorer</a></p>\n<p>Refer to\n<a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html#sysctl-kernel\">madadaidans-insecurities#sysctl-kernel</a>\nfor the following settings and their explainations.</p>\n<p>Also see the\n<a href=\"https://kspp.github.io/Recommended_Settings#sysctls\">Kernel Self Protection Projects sysctls</a></p>\n<pre><code class=\"language-nix\">  boot.kernel.sysctl = {\n    \"fs.suid_dumpable\" = 0;\n    # prevent pointer leaks\n    \"kernel.kptr_restrict\" = 2;\n    # restrict kernel log to CAP_SYSLOG capability\n    \"kernel.dmesg_restrict\" = 1;\n    # Note: certian container runtimes or browser sandboxes might rely on the following\n    # restrict eBPF to the CAP_BPF capability\n    \"kernel.unprivileged_bpf_disabled\" = 1;\n    # should be enabled along with bpf above\n    # \"net.core.bpf_jit_harden\" = 2;\n    # restrict loading TTY line disciplines to the CAP_SYS_MODULE\n    \"dev.tty.ldisk_autoload\" = 0;\n    # prevent exploit of use-after-free flaws\n    \"vm.unprivileged_userfaultfd\" = 0;\n    # kexec is used to boot another kernel during runtime and can be abused\n    \"kernel.kexec_load_disabled\" = 1;\n    # Kernel self-protection\n    # SysRq exposes a lot of potentially dangerous debugging functionality to unprivileged users\n    # 4 makes it so a user can only use the secure attention key. A value of 0 would disable completely\n    \"kernel.sysrq\" = 4;\n    # disable unprivileged user namespaces, Note: Docker, NH, and other apps may need this\n    # \"kernel.unprivileged_userns_clone\" = 0; # Set to 1 because it makes NH and other programs fail\n    # This should be set to 0 if you don't rely on flatpak, NH, Docker, etc.\n    \"kernel.unprivileged_userns_clone\" = 1;\n    # restrict all usage of performance events to the CAP_PERFMON capability\n    \"kernel.perf_event_paranoid\" = 3;\n\n    # Network\n    # protect against SYN flood attacks (denial of service attack)\n    \"net.ipv4.tcp_syncookies\" = 1;\n    # protection against TIME-WAIT assassination\n    \"net.ipv4.tcp_rfc1337\" = 1;\n    # enable source validation of packets received (prevents IP spoofing)\n    \"net.ipv4.conf.default.rp_filter\" = 1;\n    \"net.ipv4.conf.all.rp_filter\" = 1;\n\n    \"net.ipv4.conf.all.accept_redirects\" = 0;\n    \"net.ipv4.conf.default.accept_redirects\" = 0;\n    \"net.ipv4.conf.all.secure_redirects\" = 0;\n    \"net.ipv4.conf.default.secure_redirects\" = 0;\n    # Protect against IP spoofing\n    \"net.ipv6.conf.all.accept_redirects\" = 0;\n    \"net.ipv6.conf.default.accept_redirects\" = 0;\n    \"net.ipv4.conf.all.send_redirects\" = 0;\n    \"net.ipv4.conf.default.send_redirects\" = 0;\n\n    # prevent man-in-the-middle attacks\n    \"net.ipv4.icmp_echo_ignore_all\" = 1;\n\n    # ignore ICMP request, helps avoid Smurf attacks\n    \"net.ipv4.conf.all.forwarding\" = 0;\n    \"net.ipv4.conf.default.accept_source_route\" = 0;\n    \"net.ipv4.conf.all.accept_source_route\" = 0;\n    \"net.ipv6.conf.all.accept_source_route\" = 0;\n    \"net.ipv6.conf.default.accept_source_route\" = 0;\n    # Reverse path filtering causes the kernel to do source validation of\n    \"net.ipv6.conf.all.forwarding\" = 0;\n    \"net.ipv6.conf.all.accept_ra\" = 0;\n    \"net.ipv6.conf.default.accept_ra\" = 0;\n\n    ## TCP hardening\n    # Prevent bogus ICMP errors from filling up logs.\n    \"net.ipv4.icmp_ignore_bogus_error_responses\" = 1;\n\n    # Userspace\n    # restrict usage of ptrace\n    \"kernel.yama.ptrace_scope\" = 2;\n\n    # ASLR memory protection (64-bit systems)\n    \"vm.mmap_rnd_bits\" = 32;\n    \"vm.mmap_rnd_compat_bits\" = 16;\n\n    # only permit symlinks to be followed when outside of a world-writable sticky directory\n    \"fs.protected_symlinks\" = 1;\n    \"fs.protected_hardlinks\" = 1;\n    # Prevent creating files in potentially attacker-controlled environments\n    \"fs.protected_fifos\" = 2;\n    \"fs.protected_regular\" = 2;\n\n    # Randomize memory\n    \"kernel.randomize_va_space\" = 2;\n    # Exec Shield (Stack protection)\n    \"kernel.exec-shield\" = 1;\n\n    ## TCP optimization\n    # TCP Fast Open is a TCP extension that reduces network latency by packing\n    # data in the sender’s initial TCP SYN. Setting 3 = enable TCP Fast Open for\n    # both incoming and outgoing connections:\n    \"net.ipv4.tcp_fastopen\" = 3;\n    # Bufferbloat mitigations + slight improvement in throughput &amp; latency\n    \"net.ipv4.tcp_congestion_control\" = \"bbr\";\n    \"net.core.default_qdisc\" = \"cake\";\n  };\n</code></pre>\n<blockquote>\n<p>❗ Note: The above settings are fairly aggressive and can break common\nprograms, read the comment warnings.</p>\n</blockquote>\n<hr />\n<h2>Hardening Boot Parameters</h2>\n<p><code>boot.kernelParams</code> can be used to set additional kernel command line arguments\nat boot time. It can only be used for built-in modules.</p>\n<p>You can find the following settings in the\n<a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html#boot-parameters\">Boot parameters section</a></p>\n<pre><code class=\"language-nix\"># boot.nix\n      boot.kernelParams = [\n        # make it harder to influence slab cache layout\n        \"slab_nomerge\"\n        # enables zeroing of memory during allocation and free time\n        # helps mitigate use-after-free vulnerabilaties\n        \"init_on_alloc=1\"\n        \"init_on_free=1\"\n        # randomizes page allocator freelist, improving security by\n        # making page allocations less predictable\n        \"page_alloc.shuffel=1\"\n        # enables Kernel Page Table Isolation, which mitigates Meltdown and\n        # prevents some KASLR bypasses\n        \"pti=on\"\n        # randomizes the kernel stack offset on each syscall\n        # making attacks that rely on a deterministic stack layout difficult\n        \"randomize_kstack_offset=on\"\n        # disables vsyscalls, they've been replaced with vDSO\n        \"vsyscall=none\"\n        # disables debugfs, which exposes sensitive info about the kernel\n        \"debugfs=off\"\n        # certain exploits cause an \"oops\", this makes the kernel panic if an \"oops\" occurs\n        \"oops=panic\"\n        # only alows kernel modules that have been signed with a valid key to be loaded\n        # making it harder to load malicious kernel modules\n        # can make VirtualBox or Nvidia drivers unusable\n        \"module.sig_enforce=1\"\n        # prevents user space code excalation\n        \"lockdown=confidentiality\"\n        # \"rd.udev.log_level=3\"\n        # \"udev.log_priority=3\"\n      ];\n</code></pre>\n<p>This is a thoughtful start to hardening boot parameters, there are more\nrecommendations in the guide.</p>\n<p>Kernel modules for hardware devices are generally loaded automatically by\n<code>udev</code>. You can force a module to be loaded via <code>boot.kernelModules</code>.</p>\n<hr />\n<p><strong>Hardening Modprobe</strong></p>\n<p>You can use both <code>extraModprobeConfig</code> &amp; <code>blacklistedKernelModules</code> to disable\ndifferent features. If you prefer, you can place these in the next section as\nwell.</p>\n<pre><code class=\"language-nix\">boot.extraModprobeConfig = ''\n     # firewire and thunderbolt\n    install firewire-core /bin/false\n    install firewire_core /bin/false\n    install firewire-ohci /bin/false\n    install firewire_ohci /bin/false\n    install firewire_sbp2 /bin/false\n    install firewire-sbp2 /bin/false\n    install firewire-net /bin/false\n    install thunderbolt /bin/false\n    install ohci1394 /bin/false\n    install sbp2 /bin/false\n    install dv1394 /bin/false\n    install raw1394 /bin/false\n    install video1394 /bin/false\n'';\n# OR\n#boot.blacklistedKernelModules = [\n#  \"firewire-core\"\n#  # ... snip ...\n#];\n</code></pre>\n<hr />\n<p><strong>Blacklisting Kernel Parameters</strong></p>\n<p>Blacklisting unused kernel modules reduces the attack surface.</p>\n<p><a href=\"https://nixos.org/manual/nixos/stable/options#opt-boot.blacklistedKernelModules\">boot.blacklistedKernelModules</a>:\nList of names of kernel modules that should not be loaded automatically by the\nhardware probing code.</p>\n<p>You can find the following settings in the\n<a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html#kasr-kernel-modules\">Blacklisting Kernel Modules Section</a></p>\n<pre><code class=\"language-nix\">      boot.blacklistedKernelModules = [\n        # Obscure networking protocols\n        \"dccp\"   # Datagram Congestion Control Protocol\n        \"sctp\"  # Stream Control Transmission Protocol\n        \"rds\"  # Reliable Datagram Sockets\n        \"tipc\"  # Transparent Inter-Process Communication\n        \"n-hdlc\" # High-level Data Link Control\n        \"ax25\"  # Amateur X.25\n        \"netrom\"  # NetRom\n        \"x25\"     # X.25\n        \"rose\"\n        \"decnet\"\n        \"econet\"\n        \"af_802154\"  # IEEE 802.15.4\n        \"ipx\"  # Internetwork Packet Exchange\n        \"appletalk\"\n        \"psnap\"  # SubnetworkAccess Protocol\n        \"p8023\"  # Novell raw IEE 802.3\n        \"p8022\"  # IEE 802.3\n        \"can\"   # Controller Area Network\n        \"atm\"\n        # Various rare filesystems\n        \"cramfs\"\n        \"freevxfs\"\n        \"jffs2\"\n        \"hfs\"\n        \"hfsplus\"\n        \"udf\"\n\n        # \"squashfs\"  # compressed read-only file system used for Live CDs\n        # \"cifs\"  # cmb (Common Internet File System)\n        # \"nfs\"  # Network File System\n        # \"nfsv3\"\n        # \"nfsv4\"\n        # \"ksmbd\"  # SMB3 Kernel Server\n        # \"gfs2\"  # Global File System 2\n        # vivid driver is only useful for testing purposes and has been the\n        # cause of privilege escalation vulnerabilities\n        # \"vivid\"\n      ];\n</code></pre>\n<p>As with the <code>kernelParameters</code> above, there are more suggestions in the guide, I\nhave used the above parameters along with the commented out ones and had no\nissues.</p>\n<p>Also see\n<a href=\"https://github.com/secureblue/secureblue/blob/live/files/system/etc/modprobe.d/blacklist.conf\">SecureBlue’s blacklist.conf</a>\nfor more ideas.</p>\n<hr />\n<h2>Hardened Memory Allocator</h2>\n<blockquote class=\"markdown-alert-note\">\n<p>There is a performance cost to enabling a hardened memory allocator, and\nsome apps will not work without a workaround such as Firefox, Thunderbird,\nTorbrowser, LibreWolf, and ZenBrowser to name a few.</p>\n</blockquote>\n<p>With memory corruption bugs being the leading zero day category, it’s clearly\nsomething that you should be concerned with.</p>\n<p>The grapheneOS <code>hardened_malloc</code> is available for NixOS in two variants, add\neither to your <code>configuration.nix</code> or equivalent to apply them:</p>\n<ol>\n<li>\n<p><code>environment.memoryAllocator.provider = \"graphene-hardened\";</code>: This is the\ndefault configuration template that has all normal optional security features\nenabled. It’s aggressive, you can expect app breakage and a performance cost.</p>\n</li>\n<li>\n<p><code>environment.memoryAllocator.provider = \"graphene-hardened-light\";</code>: The\nlight template disables the slap quarantines, write after free check, slot\nrandomization and raises the guard slab interval from 1 to 8 but leaves\nzero-on-free and slab canaries enabled. This version has solid performance\nand is still far more secure than the standard allocator.</p>\n</li>\n</ol>\n<p><code>libhardened_malloc.so</code> is typically installed to\n<code>/usr/local/lib/libhardened_malloc.so</code> and referenced from <code>/etc/ld.so.preload</code>.</p>\n<ul>\n<li>\n<p><a href=\"https://nixos.org/manual/nixos/stable/options#opt-environment.memoryAllocator.provider\">NixOS Manual memoryAllocator</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/GrapheneOS/hardened_malloc?tab=readme-ov-file#traditional-linux-based-operating-systems\">GrapheneOS hardened_malloc</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/secureblue/secureblue/issues/193#issuecomment-1953323680\">GrapheneOS/secureblue discussion on hardened_malloc issues</a></p>\n</li>\n<li>\n<p><a href=\"https://man7.org/linux/man-pages/man8/ld.so.8.html\">ld.so man page</a></p>\n</li>\n<li>\n<p><a href=\"https://www.synacktiv.com/en/publications/exploring-grapheneos-secure-allocator-hardened-malloc\">Exploring hardened_malloc</a></p>\n</li>\n</ul>\n<hr />\n<h2>Hardening Systemd</h2>\n<!-- ![Hacker](../images/hacker.png) -->\n<p><code>systemd</code> is the core “init system” and service manager that controls how\nservices, daemons, and basic system processes are started, stopped and\nsupervised on modern Linux distributions, including NixOS. It provides a suite\nof basic building blocks for a Linux system as well as a system and service\nmanager that runs as <code>PID 1</code> and starts the rest of the system.</p>\n<p>Stage 1 (initrd) is now based on systemd by default, the old scripted\nimplimentation is deprecated.</p>\n<p>Because it launches and supervises almost all system services, hardening systemd\nmeans raising the baseline security of your entire system.</p>\n<p>Disable coredumps:</p>\n<pre><code class=\"language-nix\">systemd.coredump.enable = false;\n# ➡️ Sets the kernel's resource limit (ulimit -c 0)\n  security.pam.loginLimits = [\n    {\n      domain = \"*\"; # Applies to all users/sessions\n      type = \"-\"; # Set both soft and hard limits\n      item = \"core\"; # The soft/hard limit item\n      value = \"0\";   # Core dumps size is limited to 0 (effectively disabled)\n    }\n  ];\n</code></pre>\n<p>Disabling coredumps helps save space and improves security/privacy because when\na program fails, a coredump contains an exact copy of a programs running memory\nat the time of the crash. This can inadvertently expose sensitive data.</p>\n<p>If a program is handling private information when it crashes, the core dump file\ncould contain:</p>\n<ul>\n<li>\n<p><strong>Passwords</strong>: Stored in memory before being sent or hashed.</p>\n</li>\n<li>\n<p><strong>Encryption Keys</strong>: Used for securing network connections.</p>\n</li>\n<li>\n<p><strong>Personal Info</strong>: Chat messages, website forms, etc.</p>\n</li>\n</ul>\n<p>It can give a minor performance upgrade and does reduce the attack surface. If a\nmalicious program were to gain access to your system, one of the first things it\nmight look for are core dump files to extract sensitive data. By disabling them,\nyou eliminate this potential source of information leakage.</p>\n<p><del><code>dbus-broker</code> is generally considered more secure and robust but isn’t the\ndefault as of yet.</del></p>\n<p><code>dbus-broker</code> is now the default, it’s faster and more reliable.</p>\n<pre><code class=\"language-nix\">  users.groups.netdev = {};\n  services = {\n    dbus.implementation = \"broker\";\n    logrotate.enable = true;\n    journald = {\n      storage = \"volatile\"; # Store logs in memory\n      upload.enable = false; # Disable remote log upload (the default)\n      extraConfig = ''\n        SystemMaxUse=500M\n        SystemMaxFileSize=50M\n      '';\n    };\n  };\n</code></pre>\n<ul>\n<li>\n<p><code>dbus-broker</code> is more resilient to resource exhaustion attacks and integrates\nbetter with Linux security features.</p>\n</li>\n<li>\n<p><a href=\"https://dvdhrm.github.io/rethinking-the-dbus-message-bus/\">Rethinking-the-dbus-message-bus</a></p>\n</li>\n<li>\n<p>Setting <code>storage = \"volatile\"</code> tells journald to keep log data only in memory.\nThere is a tradeoff though, If you need long-term auditing or troubleshooting\nafter a reboot, this will not preserve system logs.</p>\n</li>\n<li>\n<p><code>upload.enable</code> is for forwarding log messages to remote servers, setting this\nto false prevents accidental leaks of potentially sensitive or internal system\ninformation.</p>\n</li>\n<li>\n<p>Enabling <code>logrotate</code> prevents your disk from filling with excessive\n<strong>legacy/service</strong> log files. These are the classic plain-text logs.</p>\n</li>\n<li>\n<p>Systemd uses <code>journald</code> which stores logs in a binary format</p>\n</li>\n</ul>\n<p>You can check the security status with:</p>\n<pre><code class=\"language-bash\">systemd-analyze security\n# or for a detailed view of individual services security posture\nsystemd-analyze security NetworkManager\n</code></pre>\n<p>Optionally disable vulnerable services to reduce the attack surface, obviously\ndon’t disable what you need, or change your habits:</p>\n<pre><code class=\"language-nix\">services = {\n    # mDNS/DNS-SD\n    avahi.enable = false;\n    # Geoclue (location services)\n    geoclue2.enable = false;\n    udisks2.enable = false;\n    accounts-daemon.enable = false;\n  };\n  # Only needed for WWAN/3G/4G modems, otherwise it runs `mmcli` unnecessarily\n  networking.modemmanager.enable = false;\n  # Bluetooth has a long history of vulnerabilities\n  hardware.bluetooth.enable = false;\n  # Prefer manual upgrades on a hardened system\n  system.autoUpgrade.enable = false;\n</code></pre>\n<p>Further reading on systemd:</p>\n<details>\n<summary> ✔️ Click to Expand Systemd Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://systemd.io/\">systemd.io</a></p>\n</li>\n<li>\n<p><a href=\"https://0pointer.de/blog/projects/systemd.html\">Rethinking PID 1</a></p>\n</li>\n<li>\n<p><a href=\"https://0pointer.de/blog/projects/the-biggest-myths.html\">Biggest Myths about Systemd</a></p>\n</li>\n</ul>\n</details>\n<p>The following is a repo containing many of the Systemd hardening settings in\nNixOS format:</p>\n<p><a href=\"https://github.com/wallago/nix-system-services-hardened\">nix-system-services-hardened</a></p>\n<p>For example, to harden bluetooth you could add the following to your\n<code>configuration.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\">systemd.services = {\n      bluetooth.serviceConfig = {\n      ProtectKernelTunables = lib.mkDefault true;\n      ProtectKernelModules = lib.mkDefault true;\n      ProtectKernelLogs = lib.mkDefault true;\n      ProtectHostname = true;\n      ProtectControlGroups = true;\n      ProtectProc = \"invisible\";\n      SystemCallFilter = [\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n        \"~@swap\"\n        \"~@reboot\"\n        \"~@mount\"\n      ];\n      SystemCallArchitectures = \"native\";\n    };\n}\n</code></pre>\n<p>As you can see from above, you typically use the <code>serviceConfig</code> attribute to\nharden settings for systemd services.</p>\n<pre><code class=\"language-bash\">systemd-analyze security bluetooth\n→ Overall exposure level for bluetooth.service: 3.3 OK 🙂\n</code></pre>\n<details>\n<summary> Click to expand `systemd.nix` example implementing many of the recommendations </summary>\n<pre><code class=\"language-nix\">{lib, ...}: {\n  systemd.services = {\n    # \"home-manager-jr\".after = [\"network-online.target\"];\n    # \"home-manager-jr\".wantedBy = [\"multi-user.target\"];\n    \"user@\".serviceConfig = {\n      ProtectSystem = \"strict\";\n      ProtectClock = true;\n      ProtectHostname = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      ProtectProc = \"invisible\";\n      PrivateTmp = true;\n      PrivateNetwork = true;\n      MemoryDenyWriteExecute = false;\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n        \"AF_NETLINK\"\n        \"AF_BLUETOOTH\"\n      ];\n      RestrictNamespaces = true;\n      RestrictRealtime = true;\n      RestrictSUIDSGID = true;\n      SystemCallFilter = [\n        \"~@keyring\"\n        \"~@swap\"\n        \"~@debug\"\n        \"~@module\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n      ];\n      SystemCallArchitectures = \"native\";\n    };\n    acpid.serviceConfig = {\n      ProtectSystem = \"full\";\n      ProtectHome = true;\n      RestrictAddressFamilies = [\"AF_INET\" \"AF_INET6\"];\n      SystemCallFilter = \"~@clock @cpu-emulation @debug @module @mount @raw-io @reboot @swap\";\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n    };\n\n    auditd.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"full\";\n      ProtectHome = true;\n      ProtectHostname = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectControlGroups = true;\n      ProtectProc = \"invisible\";\n      ProtectClock = true;\n      PrivateTmp = true;\n      PrivateNetwork = true;\n      PrivateMounts = true;\n      PrivateDevices = true;\n      RestrictNamespaces = true;\n      RestrictRealtime = true;\n      RestrictSUIDSGID = true;\n      RestrictAddressFamilies = [\n        \"~AF_INET6\"\n        \"~AF_INET\"\n        \"~AF_PACKET\"\n      ];\n      MemoryDenyWriteExecute = true;\n      LockPersonality = true;\n      SystemCallFilter = [\n        \"~@clock\"\n        \"~@module\"\n        \"~@mount\"\n        \"~@swap\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n      ];\n      SystemCallArchitectures = \"native\";\n      CapabilityBoundingSet = [\n        \"~CAP_CHOWN\"\n        \"~CAP_FSETID\"\n        \"~CAP_SETFCAP\"\n      ];\n    };\n\n    cups.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"full\";\n      ProtectHome = true;\n      ProtectKernelModules = true;\n      ProtectKernelTunables = true;\n      ProtectKernelLogs = true;\n      ProtectControlGroups = true;\n      ProtectHostname = true;\n      ProtectClock = true;\n      ProtectProc = \"invisible\";\n      RestrictRealtime = true;\n      RestrictNamespaces = true;\n      RestrictSUIDSGID = true;\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n        \"AF_NETLINK\"\n        \"AF_INET\"\n        \"AF_INET6\"\n        \"AF_PACKET\"\n      ];\n\n      MemoryDenyWriteExecute = true;\n      SystemCallFilter = [\n        \"~@clock\"\n        \"~@reboot\"\n        \"~@debug\"\n        \"~@module\"\n        \"~@swap\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n      ];\n      SystemCallArchitectures = \"native\";\n      LockPersonality = true;\n    };\n\n    NetworkManager.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectHome = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      ProtectControlGroups = true;\n      ProtectClock = true;\n      ProtectHostname = true;\n      ProtectProc = \"invisible\";\n      PrivateTmp = true;\n      RestrictRealtime = true;\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n        \"AF_NETLINK\"\n        \"AF_INET\"\n        \"AF_INET6\"\n        \"AF_PACKET\"\n      ];\n      RestrictNamespaces = true;\n      RestrictSUIDSGID = true;\n      MemoryDenyWriteExecute = true;\n      SystemCallFilter = [\n        \"~@mount\"\n        \"~@module\"\n        \"~@swap\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n        \"ptrace\"\n      ];\n      SystemCallArchitectures = \"native\";\n      LockPersonality = true;\n    };\n\n    wpa_supplicant.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"strict\";\n      ProtectHome = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      ProtectControlGroups = true;\n      ProtectClock = true;\n      ProtectHostname = true;\n      ProtectProc = \"invisible\";\n      PrivateTmp = true;\n      PrivateMounts = true;\n      RestrictRealtime = true;\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n        \"AF_NETLINK\"\n        \"AF_INET\"\n        \"AF_INET6\"\n        \"AF_PACKET\"\n      ];\n      RestrictNamespaces = true;\n      RestrictSUIDSGID = true;\n      MemoryDenyWriteExecute = true;\n      SystemCallFilter = [\n        \"~@mount\"\n        \"~@raw-io\"\n        \"~@privileged\"\n        \"~@keyring\"\n        \"~@reboot\"\n        \"~@module\"\n        \"~@swap\"\n        \"~@resources\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n        \"ptrace\"\n      ];\n      SystemCallArchitectures = \"native\";\n      LockPersonality = true;\n      CapabilityBoundingSet = \"CAP_NET_ADMIN CAP_NET_RAW\";\n    };\n\n    dbus.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"stric\";\n      ProtectControlGroups = true;\n      ProtectHome = true;\n      ProtectHostname = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      PrivateMounts = true;\n      PrivateDevices = true;\n      PrivateTmp = true;\n      RestrictSUIDSGID = true;\n      RestrictRealtime = true;\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n      ];\n      RestrictNamespaces = true;\n      SystemCallErrorNumber = \"EPERM\";\n      SystemCallArchitectures = \"native\";\n      SystemCallFilter = [\n        \"~@obsolete\"\n        \"~@resources\"\n        \"~@debug\"\n        \"~@mount\"\n        \"~@reboot\"\n        \"~@swap\"\n        \"~@cpu-emulation\"\n      ];\n      LockPersonality = true;\n      IPAddressDeny = [\"0.0.0.0/0\" \"::/0\"];\n      MemoryDenyWriteExecute = true;\n      DevicePolicy = \"closed\";\n      UMask = 0077;\n    };\n\n    nscd.serviceConfig = {\n      ProtectClock = true;\n      ProtectHostname = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      ProtectControlGroups = true;\n      ProtectProc = \"invisible\";\n      RestrictNamespaces = true;\n      RestrictRealtime = true;\n      MemoryDenyWriteExecute = true;\n      LockPersonality = true;\n      SystemCallFilter = [\n        \"~@mount\"\n        \"~@swap\"\n        \"~@clock\"\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n      ];\n      SystemCallArchitectures = \"native\";\n      CapabilityBoundingSet = [\n        \"~CAP_CHOWN\"\n        \"~CAP_FSETID\"\n        \"~CAP_SETFCAP\"\n      ];\n    };\n    bluetooth.serviceConfig = {\n      ProtectKernelTunables = lib.mkDefault true;\n      ProtectKernelModules = lib.mkDefault true;\n      ProtectKernelLogs = lib.mkDefault true;\n      ProtectHostname = true;\n      ProtectControlGroups = true;\n      ProtectProc = \"invisible\";\n      SystemCallFilter = [\n        \"~@obsolete\"\n        \"~@cpu-emulation\"\n        \"~@swap\"\n        \"~@reboot\"\n        \"~@mount\"\n      ];\n      SystemCallArchitectures = \"native\";\n    };\n    systemd-rfkill.serviceConfig = {\n      ProtectSystem = \"strict\";\n      ProtectHome = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectControlGroups = true;\n      ProtectClock = true;\n      ProtectProc = \"invisible\";\n      ProcSubset = \"pid\";\n      PrivateTmp = true;\n      MemoryDenyWriteExecute = true;\n      NoNewPrivileges = true;\n      LockPersonality = true;\n      RestrictRealtime = true;\n      SystemCallArchitectures = \"native\";\n      UMask = \"0077\";\n      IPAddressDeny = \"any\";\n    };\n    systemd-machined.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"strict\";\n      ProtectHome = true;\n      ProtectClock = true;\n      ProtectHostname = true;\n      ProtectKernelTunables = true;\n      ProtectKernelModules = true;\n      ProtectKernelLogs = true;\n      ProtectProc = \"invisible\";\n      PrivateTmp = true;\n      PrivateMounts = true;\n      PrivateUsers = true;\n      PrivateNetwork = true;\n      RestrictNamespaces = true;\n      RestrictRealtime = true;\n      RestrictSUIDSGID = true;\n      RestrictAddressFamilies = [\"AF_UNIX\"];\n      MemoryDenyWriteExecute = true;\n      SystemCallArchitectures = \"native\";\n    };\n    systemd-udevd.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectSystem = \"strict\";\n      ProtectHome = true;\n      ProtectKernelLogs = true;\n      ProtectControlGroups = true;\n      ProtectClock = true;\n      ProtectProc = \"invisible\";\n      RestrictNamespaces = true;\n      CapabilityBoundingSet = \"~CAP_SYS_PTRACE ~CAP_SYS_PACCT\";\n    };\n    nix-daemon.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectControlGroups = true;\n      ProtectKernelModules = true;\n      PrivateMounts = true;\n      PrivateTmp = true;\n      PrivateDevices = true;\n      RestrictSUIDSGID = true;\n      RestrictRealtime = true;\n      RestrictNamespaces = [\"~cgroup\"];\n      RestrictAddressFamilies = [\n        \"AF_UNIX\"\n        \"AF_NETLINK\"\n        \"AF_INET6\"\n        \"AF_INET\"\n      ];\n      CapabilityBoundingSet = [\n        \"~CAP_SYS_CHROOT\"\n        \"~CAP_BPF\"\n        \"~CAP_AUDIT_WRITE\"\n        \"~CAP_AUDIT_CONTROL\"\n        \"~CAP_AUDIT_READ\"\n        \"~CAP_SYS_PTRACE\"\n        \"~CAP_SYS_NICE\"\n        \"~CAP_SYS_RESOURCE\"\n        \"~CAP_SYS_RAWIO\"\n        \"~CAP_SYS_TIME\"\n        \"~CAP_SYS_PACCT\"\n        \"~CAP_LINUX_IMMUTABLE\"\n        \"~CAP_IPC_LOCK\"\n        \"~CAP_WAKE_ALARM\"\n        \"~CAP_SYS_TTY_CONFIG\"\n        \"~CAP_SYS_BOOT\"\n        \"~CAP_LEASE\"\n        \"~CAP_BLOCK_SUSPEND\"\n        \"~CAP_MAC_ADMIN\"\n        \"~CAP_MAC_OVERRIDE\"\n      ];\n      SystemCallErrorNumber = \"EPERM\";\n      SystemCallArchitectures = \"native\";\n      SystemCallFilter = [\n        \"~@resources\"\n        \"~@module\"\n        \"~@obsolete\"\n        \"~@debug\"\n        \"~@reboot\"\n        \"~@swap\"\n        \"~@cpu-emulation\"\n        \"~@clock\"\n        \"~@raw-io\"\n      ];\n      LockPersonality = true;\n      MemoryDenyWriteExecute = false;\n      DevicePolicy = \"closed\";\n      UMask = 0077;\n    };\n    systemd-journald.serviceConfig = {\n      NoNewPrivileges = true;\n      ProtectProc = \"invisible\";\n      ProtectHostname = true;\n      PrivateMounts = true;\n    };\n  };\n}\n</code></pre>\n</details>\n<hr />\n<h2>Lynis and other tools</h2>\n<p>Lynis is a security auditing tool for systems based on UNIX like Linux, macOS,\nBSD, and others.–<a href=\"https://github.com/CISOfy/lynis\">lynis repo</a></p>\n<p><code>chkrootkit</code> was removed as it is unmaintained and archived upstream.</p>\n<p>Installation:</p>\n<pre><code class=\"language-nix\">environment.systemPackages = [\npkgs.lynis\npkgs.clamav\npkgs.aide\n ];\n</code></pre>\n<details>\n<summary> ✔️ Click to Expand AIDE Example </summary>\n<p>AIDE is an intrusion detection system (IDS) that will notify us whenever it\ndetects that a potential intrusion has occurred. When a system is compromised,\nattackers typically will try to change file permissions and escalate to the root\nuser account and start to modify system files, AIDE can detect this.</p>\n<p>To set up AIDE on your system follow these steps:</p>\n<ol>\n<li>Create the <code>aide.conf</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mkdir -p /var/lib/aide &amp;&amp; cd /var/lib/aide/\nsudo hx aide.conf\n</code></pre>\n<p>Add the following content to <code>/var/lib/aide/aide.conf</code>:</p>\n<pre><code class=\"language-text\"># aide.conf\n# Example configuration file for AIDE.\n\n@@define DBDIR /var/lib/aide\n\n# The location of the database to be read.\ndatabase_in=file:@@{DBDIR}/aide.db.gz\n\n# The location of the database to be written.\n#database_out=sql:host:port:database:login_name:passwd:table\n#database_out=file:aide.db.new\ndatabase_out=file:@@{DBDIR}/aide.db.new.gz\n\n# Whether to gzip the output to database\ngzip_dbout=yes\n\nlog_level=info\n\nreport_url=file:/var/log/aide/aide.log\nreport_url=stdout\n#report_url=stderr\n#NOT IMPLEMENTED report_url=mailto:root@foo.com\n#NOT IMPLEMENTED report_url=syslog:LOG_AUTH\n\n# These are the default rules.\n#\n#p:      permissions\n#i:      inode:\n#n:      number of links\n#u:      user\n#g:      group\n#s:      size\n#b:      block count\n#m:      mtime\n#a:      atime\n#c:      ctime\n#S:      check for growing size\n#md5:    md5 checksum\n#sha1:   sha1 checksum\n#rmd160: rmd160 checksum\n#tiger:  tiger checksum\n#haval:  haval checksum\n#gost:   gost checksum\n#crc32:  crc32 checksum\n#R:      p+i+n+u+g+s+m+c+md5\n#L:      p+i+n+u+g\n#E:      Empty group\n#&gt;:      Growing logfile p+u+g+i+n+S\n\n# You can create custom rules like this.\n\nNORMAL = R+b+sha512\n\nDIR = p+i+n+u+g\n\n# Next decide what directories/files you want in the database.\n\n/boot   NORMAL\n/bin    NORMAL\n/sbin   NORMAL\n/lib    NORMAL\n/opt    NORMAL\n/usr    NORMAL\n/root   NORMAL\n\n# Check only permissions, inode, user and group for /etc, but\n# cover some important files closely.\n/etc    p+i+u+g\n!/etc/mtab\n/etc/exports  NORMAL\n/etc/fstab    NORMAL\n/etc/passwd   NORMAL\n/etc/group    NORMAL\n/etc/gshadow  NORMAL\n/etc/shadow   NORMAL\n\n/var/log   p+n+u+g\n\n# With AIDE's default verbosity level of 5, these would give lots of\n# warnings upon tree traversal. It might change with future version.\n#\n#=/lost\\+found    DIR\n#=/home           DIR\n</code></pre>\n<p>Create the logfile:</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /var/log/aide\nsudo touch /var/log/aide/aide.log\n</code></pre>\n<ol start=\"2\">\n<li>Generate the initial database, this will store the checksums of all files\nthat it’s configured to monitor. Take note of the location of the new\ndatabase, mine was <code>/etc/aide.db.new</code></li>\n</ol>\n<pre><code class=\"language-bash\">sudo aide --config /var/lib/aide/aide.conf --init\n</code></pre>\n<ol start=\"3\">\n<li>Move the new database and remove the <code>.new</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz\n</code></pre>\n<pre><code class=\"language-bash\">ls /var/lib/aide/\naide.conf   aide.db.gz\n</code></pre>\n<ol start=\"4\">\n<li>Check with AIDE:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo aide --check --config /var/lib/aide/aide.conf\nStart timestamp: 2025-09-05 09:50:07 -0400 (AIDE 0.19.2)\nAIDE found NO differences between database and filesystem. Looks okay!!\n</code></pre>\n<ol start=\"5\">\n<li>Whenever you make changes to system files, or especially after running a\nsystem update or installing new tools, you have to rescan all files to update\ntheir checksums in the AIDE database:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo aide --update --config /var/lib/aide/aide.conf\n</code></pre>\n<p>Unfortunately, AIDE doesn’t automatically replace the old database so you have\nto rename the new one again:</p>\n<pre><code class=\"language-bash\">sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz\n</code></pre>\n<p>And finally check again:</p>\n<pre><code class=\"language-bash\">sudo aide --check --config /var/lib/aide/aide.conf\n</code></pre>\n<ul>\n<li><a href=\"https://linux.die.net/man/1/aide\">aide(1) man page</a></li>\n</ul>\n</details>\n<details>\n<summary> ✔️ Click to Expand clamav.nix Example </summary>\n<pre><code class=\"language-nix\">{pkgs, ...}: {\n  environment.systemPackages = with pkgs; [\n    clamav\n  ];\n  services.clamav = {\n    # Enable clamd daemon\n    daemon.enable = true;\n    updater.enable = true;\n    updater.frequency = 12; # Number of database checks per day\n    scanner = {\n      enable = true;\n      # 4:00 AM\n      interval = \"*-*-* 04:00:00\";\n      scanDirectories = [\n        \"/home\"\n        \"/var/lib\"\n        \"/tmp\"\n        \"/etc\"\n        \"/var/tmp\"\n      ];\n    };\n  };\n}\n</code></pre>\n</details>\n<p>Lynis Usage:</p>\n<pre><code class=\"language-bash\">sudo lynis show commands\n# Output:\nCommands:\nlynis audit\nlynis configure\nlynis generate\nlynis show\nlynis update\nlynis upload-only\n\nsudo lynis audit system\n# Output:\n  Lynis security scan details:\n\n  Hardening index : 79 [###############     ]\n  Tests performed : 234\n  Plugins enabled : 0\n\n  Components:\n  - Firewall               [V]\n  - Malware scanner        [V]\n\n  Scan mode:\n  Normal [V]  Forensics [ ]  Integration [ ]  Pentest [ ]\n\n  Lynis modules:\n  - Compliance status      [?]\n  - Security audit         [V]\n  - Vulnerability scan     [V]\n</code></pre>\n<ul>\n<li>\n<p>The “Lynis hardening index” is an overall impression on how well a system is\nhardened. However, this is just an indicator on measures taken - not a\npercentage of how safe a system might be. A score over 75 typically indicates\na system with more than average safety measures implemented.</p>\n</li>\n<li>\n<p>Lynis will give you more recommendations for securing your system as well.</p>\n</li>\n</ul>\n<p>If you use <code>clamscan</code>, create the following log file:</p>\n<pre><code class=\"language-bash\">sudo touch /var/log/clamscan.log\n</code></pre>\n<p>Example cron job for <code>clamav</code> &amp; <code>aide</code>:</p>\n<pre><code class=\"language-nix\">{pkgs, ...}: {\n  services.cron = {\n    enable = true;\n    # messages.enable = true;\n    systemCronJobs = [\n      # Every day at 2:00 AM, run clamscan as root and append output to a log file\n      \"0 2 * * * root ${pkgs.clamav}/bin/clamscan -r /home &gt;&gt; /var/log/clamscan.log\"\n      \"0 11 * * * ${pkgs.aide}/bin/aide --check --config /var/lib/aide/aide.conf\"\n    ];\n  };\n}\n</code></pre>\n<p>ClamAV usage:</p>\n<p>You can run <code>clamav</code> manually with:</p>\n<pre><code class=\"language-bash\"># Recursive Scan:\nsudo clamscan -r ~/home\n</code></pre>\n<blockquote class=\"markdown-alert-note\">\n<p>You only need either the individual <code>pkgs.clamav</code> with the cron job <strong>OR</strong> the\n<code>clamd-daemon</code> module. <code>clamdscan</code> is for software integration and\nuses a different user that doesn’t have permission to scan your files. You can\nuse <code>clamdscan --fdpass /path/to/scan</code> to pass the necessary file permissions.\n<code>clamdscan</code> runs in the background, you can watch it with <code>top</code>.</p>\n</blockquote>\n<h2>Securing SSH</h2>\n<blockquote>\n<p><strong>Security information</strong>: Changing SSH configuration settings can\nsignificantly impact the security of your system(s). It is crucial to have a\nsolid understanding of what you are doing before making any adjustments. Avoid\nblindly copying and pasting examples, including those from this Wiki page,\nwithout conducting a thorough analysis. Failure to do so may compromise the\nsecurity of your system(s) and lead to potential vulnerabilities. Take the\ntime to comprehend the implications of your actions and ensure that any\nchanges made are done thoughtfully and with care. –NixOS Wiki</p>\n</blockquote>\n<blockquote class=\"markdown-alert-note\">\n<p>Choose one, either <code>ssh-agent</code> or <code>gpg-agent</code></p>\n</blockquote>\n<ol>\n<li>Use normal SSH keys generated with <code>ssh-keygen</code>, this is recommended unless\nyou have a good reason for not using it.</li>\n</ol>\n<p><strong>OR</strong></p>\n<ol start=\"2\">\n<li>Use a GPG key with <code>gpg-agent</code> (which acts as your SSH agent). Complex, and\nharder to understand in my opinion.</li>\n</ol>\n<p>My setup caused conflicts when enabling <code>programs.ssh.startAgent</code> so I chose\n<code>gpg-agent</code> personally.</p>\n<p>There are situations where you are required to use one or the other like for\nheadless CI/CD environments, <code>ssh-keygen</code> is required.</p>\n<ul>\n<li><a href=\"https://saylesss88.github.io/nix/gpg-agent.html\">Click Here for GnuPG and gpg-agent chapter</a></li>\n</ul>\n<p>Further reading:</p>\n<details>\n<summary> ✔️ Click to Expand Resourses on OpenSSH </summary>\n<ul>\n<li>\n<p><a href=\"https://wiki.archlinux.org/title/OpenSSH\">Arch Wiki OpenSSH</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.gentoo.org/wiki/GnuPG\">Gentoo GnuPG</a></p>\n</li>\n<li>\n<p><a href=\"https://rgoulter.com/blog/posts/programming/2022-06-10-a-visual-explanation-of-gpg-subkeys.html\">A Visual Explanation of GPG Subkeys</a></p>\n</li>\n<li>\n<p><a href=\"https://blog.stribik.technology/2015/01/04/secure-secure-shell.html\">Secure Secure Shell</a></p>\n</li>\n</ul>\n</details>\n<hr />\n<h2>Key generation</h2>\n<h3>ssh-keygen</h3>\n<p>The <code>ed25519</code> algorithm is significantly faster and more secure when compared to\n<code>RSA</code>. You can also specify the key derivation function (KDF) rounds to\nstrengthen protection even more.</p>\n<p>For example, to generate a strong key for GitHub:</p>\n<pre><code class=\"language-bash\">ssh-keygen -t ed25519 -a 32 -f ~/.ssh/id_ed25519_github_$(date +%Y-%m-%d) -C \"SSH Key for GitHub\"\n</code></pre>\n<ul>\n<li>\n<p><code>-t</code> is for type</p>\n</li>\n<li>\n<p><code>-a 32</code> sets the number of KDF rounds. The standard is usually good enough,\nadding extra rounds can make it harder to brute-force.</p>\n</li>\n<li>\n<p><code>-f</code> is for filename</p>\n</li>\n</ul>\n<h3>OpenSSH Server</h3>\n<p>First of all, if you don’t use SSH don’t enable it in the first place. If you do\nuse SSH, it’s important to understand what that opens you up to.</p>\n<p>The following are some recommendations from Mozilla on OpenSSH:</p>\n<ul>\n<li><a href=\"https://infosec.mozilla.org/guidelines/openssh.html\">Mozilla OpenSSH guidelines</a></li>\n</ul>\n<p>The following OpenSSH setup is based on the above guidelines with strong\nalgorithms, and best practices: (EDITED: 10-07-25 to follow best-practices on\npost-quantum crypto)</p>\n<pre><code class=\"language-nix\">{config, ...}: {\n  config = {\n    services = {\n      fail2ban = {\n        enable = true;\n        maxretry = 5;\n        bantime = \"1h\";\n        # ignoreIP = [\n        # \"172.16.0.0/12\"\n        # \"192.168.0.0/16\"\n        # \"2601:881:8100:8de0:31e6:ac52:b5be:462a\"\n        # \"matrix.org\"\n        # \"app.element.io\" # don't ratelimit matrix users\n        # ];\n\n        bantime-increment = {\n          enable = true; # Enable increment of bantime after each violation\n          multipliers = \"1 2 4 8 16 32 64 128 256\";\n          maxtime = \"168h\"; # Do not ban for more than 1 week\n          overalljails = true; # Calculate the bantime based on all the violations\n        };\n      };\n      openssh = {\n        enable = true;\n        settings = {\n          PasswordAuthentication = false;\n          PermitEmptyPasswords = false;\n          PermitTunnel = false;\n          UseDns = false;\n          KbdInteractiveAuthentication = false;\n          X11Forwarding = config.services.xserver.enable;\n          MaxAuthTries = 3;\n          MaxSessions = 2;\n          ClientAliveInterval = 300;\n          ClientAliveCountMax = 0;\n          AllowUsers = [\"your-user\"];\n          TCPKeepAlive = false;\n          AllowTcpForwarding = false;\n          AllowAgentForwarding = false;\n          LogLevel = \"VERBOSE\";\n          PermitRootLogin = \"no\";\n          KexAlgorithms = [\n            # Post-Quantum: https://www.openssh.org/pq.html\n            \"mlkem768x25519-sha256\"\n            \"sntrup761x25519-sha512\"\n            \"curve25519-sha256@libssh.org\"\n            \"ecdh-sha2-nistp521\"\n            \"ecdh-sha2-nistp384\"\n            \"ecdh-sha2-nistp256\"\n            \"diffie-hellman-group-exchange-sha256\"\n          ];\n          Ciphers = [\n            \"aes256-gcm@openssh.com\"\n            \"aes128-gcm@openssh.com\"\n            # stream cipher alternative to aes256, proven to be resilient\n            # Very fast on basically anything\n            \"chacha20-poly1305@openssh.com\"\n            # industry standard, fast if you have AES-NI hardware\n            \"aes256-ctr\"\n            \"aes192-ctr\"\n            \"aes128-ctr\"\n          ];\n          Macs = [\n            # Combines the SHA-512 hash func with a secret key to create a MAC\n            \"hmac-sha2-512-etm@openssh.com\"\n            \"hmac-sha2-256-etm@openssh.com\"\n            \"umac-128-etm@openssh.com\"\n            \"hmac-sha2-512\"\n            \"hmac-sha2-256\"\n            \"umac-128@openssh.com\"\n          ];\n        };\n        # These keys will be generated for you\n        hostKeys = [\n          {\n            path = \"/etc/ssh/ssh_host_ed25519_key\";\n            type = \"ed25519\";\n          }\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<p>TCP port 22 (ssh) is opened automatically if the SSH daemon is enabled\n(<code>services.openssh.enable = true;</code>)</p>\n<p>Much of the SSH hardening settings came from\n<a href=\"https://ryanseipp.com/post/nixos-secure-ssh/\">ryanseipp’s secure-ssh Guide</a>\nwith some additions of my own.</p>\n<p>Fail2Ban is an intrusion prevention software framework. It’s designed to prevent\nbrute-force attacks by scanning log files for suspicious activity, such as\nrepeated failed login attempts.</p>\n<p>As of 26.05, <a href=\"https://reaction.ppom.me/\">reaction</a> was added to nixpkgs.\n“A daemon that scans program outputs for repeated patterns, and takes action.”\nIt labels itself as a more modern alternative to fail2ban, implemented in Rust.\nThe initial rust commit was authored May 20, 2024 and it has not received an\nexternal security audit yet. Worth checking out, probably not a primary\nrecommendation yet.</p>\n<p>OpenSSH is the primary tool for secure remote access for NixOS. Enabling it\nactivates the OpenSSH server on the system, allowing incoming SSH connections.</p>\n<p>The above configuration is a robust setup for securing an SSH server by:</p>\n<ul>\n<li>\n<p>Preventing brute-force attacks with Fail2Ban</p>\n</li>\n<li>\n<p>Eliminating password authentication in favor of more secure SSH keys</p>\n</li>\n<li>\n<p>Restricting user access and preventing root login</p>\n</li>\n<li>\n<p>Disabling potentially risky forwarding features (tunnel, TCP, agent)</p>\n</li>\n<li>\n<p>Enforce the use of strong, modern cryptographic algorithms for all SSH\ncommunications.</p>\n</li>\n<li>\n<p>Enhanced logging for better auditing.</p>\n</li>\n</ul>\n<p>Further Reading:</p>\n<ul>\n<li>\n<p><a href=\"https://www.openssh.com/\">OpenSSH</a></p>\n</li>\n<li>\n<p><a href=\"https://www.digitalocean.com/community/tutorials/how-fail2ban-works-to-protect-services-on-a-linux-server\">DigitalOcean how fail2ban works</a></p>\n</li>\n</ul>\n<hr />\n<h2>Encrypted Secrets</h2>\n<p>Never store secrets in plain text in repositories. Use something like\n<a href=\"https://github.com/Mic92/sops-nix\">sops-nix</a>, which lets you keep encrypted\nsecrets under version control declaratively.</p>\n<p>Another option is <a href=\"https://github.com/ryantm/agenix\">agenix</a></p>\n<ul>\n<li><a href=\"https://wiki.nixos.org/wiki/Agenix\">NixOS Wiki Agenix</a></li>\n</ul>\n<h3>Sops-nix Guide</h3>\n<p>Protect your secrets, the following guide is on setting up Sops on NixOS:\n<a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">Sops Encrypted Secrets</a></p>\n<hr />\n<h2>Auditd</h2>\n<p>To enable the Linux Audit Daemon (<code>auditd</code>) and define a very basic rule set,\nyou can use the following NixOS configuration. This example demonstrates how to\nlog every program execution (<code>execve</code>) on a 64-bit architecture.</p>\n<pre><code class=\"language-nix\"># modules/security/auditd-minimal.nix (or directly in configuration.nix)\n{\n  # start as early in the boot process as possible\n  boot.kernelParams = [\"audit=1\"];\n  security.auditd.enable = true;\n  security.audit.enable = true;\n  security.audit.rules = [\n    # Log all program executions on 64-bit architecture\n    \"-a exit,always -F arch=b64 -S execve\"\n  ];\n}\n</code></pre>\n<ul>\n<li>\n<p><code>audit=1</code> Enables auditing at the kernel level very early in the boot process.\nWithout this, some events could be missed.</p>\n</li>\n<li>\n<p><code>security.auditd.enable = true;</code> Ensures the <code>auditd</code> userspace daemon is\nstarted.</p>\n</li>\n<li>\n<p>While often enabled together, <code>security.audit.enable</code> specifically refers to\nenabling the NixOS module for audit rules generation.</p>\n</li>\n<li>\n<p><code>execve</code> (program executions)</p>\n</li>\n<li>\n<p>This is just a basic configuration, there is much more that can be tracked.</p>\n</li>\n</ul>\n<hr />\n<h2>USB Port Protection</h2>\n<p>It’s important to protect your USB ports to prevent BadUSB attacks, data\nexfiltration, unauthorized device access, malware injection, etc.</p>\n<p>To get a list of your connected USB devices you can use <code>lsusb</code> from the\n<code>usbutils</code> package.</p>\n<pre><code class=\"language-bash\">lsusb\n</code></pre>\n<p>To list the devices recognized by USBGuard, run:</p>\n<pre><code class=\"language-bash\">sudo usbguard list-devices\n</code></pre>\n<ul>\n<li><a href=\"https://mynixos.com/options/services.usbguard\">MyNixOS services.usbguard</a></li>\n</ul>\n<p>Change <code>your-user</code> to your username:</p>\n<pre><code class=\"language-nix\"># usbguard.nix\n{\n  config,\n  pkgs,\n  lib,\n  ...\n}: let\n  inherit (lib) mkIf;\n  cfg = config.custom.security.usbguard;\nin {\n  options.custom.security.usbguard = {\n    enable = lib.mkEnableOption \"usbguard\";\n  };\n\n  config = mkIf cfg.enable {\n    services.usbguard = {\n      enable = true;\n      IPCAllowedUsers = [\"root\" \"your-user\"];\n    # presentDevicePolicy refers to how to treat USB devices that are already connected when the daemon starts\n      presentDevicePolicy = \"allow\";\n      rules = ''\n        # allow `only` devices with mass storage interfaces (USB Mass Storage)\n        allow with-interface equals { 08:*:* }\n        # allow mice and keyboards\n        # allow with-interface equals { 03:*:* }\n\n        # Reject devices with suspicious combination of interfaces\n        reject with-interface all-of { 08:*:* 03:00:* }\n        reject with-interface all-of { 08:*:* 03:01:* }\n        reject with-interface all-of { 08:*:* e0:*:* }\n        reject with-interface all-of { 08:*:* 02:*:* }\n      '';\n    };\n\n    environment.systemPackages = [pkgs.usbguard];\n  };\n}\n</code></pre>\n<p>The above settings can be found in\n<a href=\"https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/security_guide/sec-using-usbguard\">RedHat UsbGuard</a></p>\n<p>The only <code>allow</code> rule is for devices with <strong>only</strong> mass storage interfaces\n(<code>08:*:*</code>) i.e., USB Mass storage devices, devices like keyboards and mice\n(which use interface class <code>03:*:*</code>) implicitly <strong>not allowed</strong>.</p>\n<p>The <code>reject</code> rules reject devices with a suspicious combination of interfaces. A\nUSB drive that implements a keyboard or a network interface is very suspicious,\nthese <code>reject</code> rules prevent that.</p>\n<p>The <code>presentDevicePolicy = \"allow\";</code> allows any device that is present at daemon\nstart up even if they’re not explicitly allowed. However, newly plugged in\ndevices must match an <code>allow</code> rule or get denied implicitly.</p>\n<p>The <code>presentDevicePolicy</code> should be one of: # one of <code>\"apply-policy\"</code>(default,\nevaluate the rule set for every present device), <code>\"block\"</code>, <code>\"reject\"</code>, <code>\"keep\"</code>\n(keep whatever state the device is currently in), or <code>\"allow\"</code>, which is used in\nthe example.</p>\n<p>There is also the\n<a href=\"https://github.com/Cropi/usbguard-notifier\">usbguard-notifier</a></p>\n<p>And enable it with the following in your <code>configuration.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\"># configuration.nix\nimports = [\n    ./usbguard.nix\n];\ncustom.security.usbguard.enable = true;\n</code></pre>\n<blockquote>\n<p>❗ If you are ever unsure about a setting that you want to harden and think\nthat it could possibly break your system you can always use a specialisation\nreversing the action and choose it’s generation at boot up. For example, to\nforce-reverse the above settings you could:</p>\n<pre><code class=\"language-nix\"># configuration.nix\nspecialisation.no-usbguard.configuration = {\n    services.usbguard.enable = lib.mkForce false;\n};\n</code></pre>\n<ul>\n<li>This is a situation where I recommend this, it’s easy to lock yourself out\nof your keyboard, mouse, etc. when trying to configure this.</li>\n</ul>\n</blockquote>\n<p>Further Reading:</p>\n<ul>\n<li>\n<p><a href=\"https://www.ninjaone.com/it-hub/endpoint-security/what-is-badusb/\">NinjaOne BadUSB</a></p>\n</li>\n<li>\n<p><a href=\"https://usbguard.github.io/\">USBGuard</a></p>\n</li>\n<li>\n<p><a href=\"https://www.cyberciti.biz/security/how-to-protect-linux-against-rogue-usb-devices-using-usbguard/\">NixCraft USBGuard</a></p>\n</li>\n</ul>\n<hr />\n<h2>Doas over sudo (Warning Doas is unmaintained)</h2>\n<details>\n<summary> ✔️ Click to Expand Unmaintained Doas example </summary>\n<blockquote class=\"markdown-alert-note\">\n<p>I have moved to <code>run0</code> for authentication which is included by default with\nsystemd. It’s actually a symlink to the existing <code>systemd-run</code> tool. It\nbehaves like a secure <code>sudo</code> alternative: it spawns a transient service under\nPID 1 for privilege escalation, without relying on SUID (set user ID)\nbinaries.</p>\n</blockquote>\n<blockquote class=\"markdown-alert-warning\">\n<p>the Nixpkgs version of <code>doas</code>,<a href=\"https://github.com/Duncaen/OpenDoas\">OpenDoas</a>\nis unmaintained and hasn’t been updated in 3 to 4 years. If you don’t like\n<code>run0</code>, consider using <code>sudo-rs</code>. I’m leaving this here for now, may remove it\nin the future to not promote using unmaintained software, you’ve been warned.</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://mastodon.social/@pid_eins/112353324518585654\">Why run0</a></p>\n</li>\n<li>\n<p>SUID = “Set User ID”: When a binary has the SUID bit set, it runs with the\nprivileges of the file’s owner (often root). There is a long history of\nvulnerabilities with SUID binaries.</p>\n</li>\n</ul>\n<p>For a more minimalist version of <code>sudo</code> with a smaller codebase and attack\nsurface, consider <code>doas</code>. Replace <code>userName</code> with your username:</p>\n<pre><code class=\"language-nix\"># doas.nix\n{\n  lib,\n  config,\n  pkgs, # Add pkgs if you need to access user information\n  ...\n}: let\n  cfg = config.custom.security.doas;\nin {\n  options.custom.security.doas = {\n    enable = lib.mkEnableOption \"doas\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    # Disable sudo\n    security.sudo.enable = false;\n\n    # Enable and configure `doas`.\n    security.doas = {\n      enable = true;\n      extraRules = [\n        {\n          # Grant doas access specifically to your user\n          users = [\"userName\"]; # &lt;--- Only give access to your user\n          # persist = true; # Convenient but less secure\n          # noPass = true;    # Convenient but even less secure\n          keepEnv = true; # Often necessary\n          # Optional: You can also specify which commands they can run, e.g.:\n          # cmd = \"ALL\"; # Allows running all commands (default if not specified)\n          # cmd = \"/run/current-system/sw/bin/nixos-rebuild\"; # Only allow specific command\n        }\n      ];\n    };\n\n    # Add an alias to the shell for backward-compat and convenience.\n    environment.shellAliases = {\n      sudo = \"doas\";\n    };\n  };\n}\n</code></pre>\n<p>You would then import this into your <code>configuration.nix</code> and enable/disable it\nwith the following:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n\nimports = [\n    ./doas.nix\n];\n\ncustom.security.doas.enable = true;\n</code></pre>\n<blockquote>\n<p>[!NOTE]: Many people opt for the less secure <code>groups = [\"wheel\"];</code> in the\nabove configuration instead of <code>users = [\"userName\"];</code> to give wider access,\nthe choice is yours.</p>\n</blockquote>\n</details>\n<hr />\n<h2>Firejail</h2>\n<blockquote>\n<p>❗️ Critics such as madaidan say that Firejail worsens security by acting as a\nprivilege escalation hole. Firejail requires the executable to be setuid,\nmeaning it runs with root privileges.This is risky because any vulnerability\nin Firejail can lead to privilege escalation. This combined with many\nconvenience features and complicated command line flags leads to a large\nattack surface.</p>\n</blockquote>\n<ul>\n<li>\n<p>I haven’t personally tried\n<a href=\"https://github.com/Naxdy/nix-bwrapper\">nix-bwrapper</a> myself yet, but it’s\nanother sandboxing option that looks interesting. Bubblewrap is known for\nhaving a more minimal design and smaller attack surface.</p>\n<ul>\n<li>Also see: <a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html#flatpak\">Flatpak section</a> for another option for sandboxing.</li>\n</ul>\n</li>\n<li>\n<p><a href=\"https://sr.ht/~fgaz/nix-bubblewrap/\">nix-bubblewrap</a> is another option.</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Firejail\">NixOS Wiki Firejail</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.archlinux.org/title/Firejail\">Arch Wiki Firejail</a></p>\n</li>\n</ul>\n<blockquote class=\"markdown-alert-warning\">\n<p>Running untrusted code is never safe, sandboxing cannot change this.\n–Arch Wiki</p>\n</blockquote>\n<pre><code class=\"language-nix\"># firejail.nix\n{\n  pkgs,\n  lib,\n  ...\n}: {\n  programs.firejail = {\n    enable = true;\n    wrappedBinaries = {\n      # Sandbox a web browser\n      librewolf = {\n        executable = \"${lib.getBin pkgs.librewolf}/bin/librewolf\";\n        profile = \"${pkgs.firejail}/etc/firejail/librewolf.profile\";\n      };\n      # Sandbox a file manager\n      thunar = {\n        executable = \"${lib.getBin pkgs.xfce.thunar}/bin/thunar\";\n        profile = \"${pkgs.firejail}/etc/firejail/thunar.profile\";\n      };\n      # Sandbox a document viewer\n      zathura = {\n        executable = \"${lib.getBin pkgs.zathura}/bin/zathura\";\n        profile = \"${pkgs.firejail}/etc/firejail/zathura.profile\";\n      };\n    };\n  };\n}\n</code></pre>\n<p><code>wrappedBinaries</code> is a list of applications you want to run inside a sandbox.\nOnly the apps in the <code>wrappedBinaries</code> attribute set will be automatically\nfirejailed when launched the usual way.</p>\n<p>Other apps may be started manually using <code>firejail &lt;app&gt;</code>, or added to\n<code>wrappedBinaries</code> if you want automatic sandboxing, just make sure the profile\nexists.</p>\n<p>To inspect which profiles are available, after rebuilding go to <code>/nix/store/</code>, I\nused Yazi to search for <code>/firejail</code> and followed it to <code>firejail/etc</code>, where the\nprofiles are.</p>\n<p>There are many flags and options available with firejail, I suggest checking out\n<code>man firejail</code>.</p>\n<p>There are comments explaining what’s going on in:\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/fi/firejail/package.nix\">firejail/package.nix</a></p>\n<p>Firejail is a SUID program that reduces the risk of security breaches by\nrestricting the running environment of untrusted applications using\n<a href=\"https://lwn.net/Articles/531114/\">Linux namespaces</a> and\n<a href=\"https://l3net.wordpress.com/2015/04/13/firejail-seccomp-guide/\">seccomp-bpf</a>–<a href=\"https://firejail.wordpress.com/\">Firejail Security Sandbox</a></p>\n<p>It provides sandboxing and access restriction per application, much like what\nAppArmor/SELinux does at a kernel level. However, it’s not as secure or\ncomprehensive as kernel-enforced MAC systems (AppArmor/SELinux), since it’s a\nuserspace tool and can potentially be bypassed by privilege escalation exploits.</p>\n<hr />\n<h2>Flatpak</h2>\n<blockquote class=\"markdown-alert-note\">\n<p>You cannot effectively use Firejail with Flatpak apps because of how\ntheir sandboxing technologies operate. Flatpak also won’t work with the\nhardened kernel because they require unprivileged user namespaces which the\nhardened kernel completely disables.</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://docs.flatpak.org/en/latest/sandbox-permissions.html#permissions-guidelines\">Flatpak permissions &amp; What they Do</a>\nReference this while setting permissions with Flatseal, many apps come with\nmore permissions than they need to function effectively breaking the sandbox.</p>\n</li>\n<li>\n<p><a href=\"https://docs.flatpak.org/en/latest/sandbox-permissions.html#portals\">Portals</a>\nprovide mediated, user-controlled access to host resources outside the\nsandbox, so apps don’t need broad blanket permissions.</p>\n</li>\n</ul>\n<p>Apps that don’t have a flatpak equivalent can be further hardened with\nbubblewrap independently but bubblewrap is not needed on Flatpak apps.</p>\n<p>Because of this limited native MAC (Mandatory Access Control) support on NixOS,\nusing Flatpak is often a good approach to get sandboxing and isolation for many\nGUI apps.</p>\n<ul>\n<li>\n<p>Flatpak bundles runtimes and sandbox mechanisms that provide app isolation\nindependently of the host system’s AppArmor or SELinux infrastructure. This\ncan improve security and containment for GUI applications running on NixOS\ndespite the system lacking full native MAC coverage.</p>\n</li>\n<li>\n<p>Flatpak apps benefit from sandboxing through bubblewrap, which isolate apps\nand restrict access to user/home and system resources.</p>\n</li>\n</ul>\n<p>Add Flatpak with the FlatHub repository for all users:</p>\n<pre><code class=\"language-nix\">services.flatpak.enable = true;\n  systemd.services.flatpak-repo = {\n    wantedBy = [ \"multi-user.target\" ];\n    path = [ pkgs.flatpak ];\n    script = ''\n      flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo\n      # Only apps that are verified\n      # flatpak remote-add --if-not-exists --subset=verified flathub-verified https://flathub.org/repo/flathub.flatpakrepo\n    '';\n  };\nxdg = {\n  portal = {\n    enable = true;\n    extraPortals = [ pkgs.xdg-desktop-portal-gtk ];\n    config.common.default = [ \"gtk\" ];\n  };\n};\n# Disables screencopy\nsystemd.user.services.\"xdg-desktop-portal-wlr\" = {\n  enable = false;\n};\n</code></pre>\n<details>\n<summary> ✔️ declarative-flatpak </summary>\n<ol>\n<li>Add the flake input to your <code>flake.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">inputs = {\n  flatpaks.url = \"github:in-a-dil-emma/declarative-flatpak/latest\";\n};\n</code></pre>\n<ol start=\"2\">\n<li>The following is a NixOS module that installs Firefox &amp; Bitwarden:</li>\n</ol>\n<pre><code class=\"language-nix\"># flatpak.nix\n{\n  pkgs,\n  inputs,\n  ...\n}: {\n  imports = [\n    inputs.flatpaks.nixosModules.default\n  ];\n  services.flatpak = {\n    enable = true;\n    remotes = {\n      \"flathub\" = \"https://dl.flathub.org/repo/flathub.flatpakrepo\";\n      # \"flathub-beta\" = \"https://dl.flathub.org/beta-repo/flathub-beta.flatpakrepo\";\n    };\n    packages = [\n      \"flathub:app/org.mozilla.firefox//stable\"\n      \"flathub:app/com.bitwarden.desktop//stable\"\n      # \"flathub-beta:app/org.kde.kdenlive/x86_64/stable\"\n      # \":${./foobar.flatpak}\"\n      \"flathub:/root/testflatpak.flatpakref\"\n    ];\n    overrides = {\n      # note: \"global\" is a flatpak thing\n      # if you ever ran \"flatpak override\" without specifying a ref you will know\n      \"global\".Context = {\n        filesystems = [\n          \"home\"\n        ];\n        sockets = [\n          \"!wayland\"\n          \"!fallback-x11\"\n        ];\n      };\n      \"org.mozilla.Firefox\" = {\n        Environment = {\n          \"MOZ_ENABLE_WAYLAND\" = 1;\n        };\n        Context.sockets = [\n          \"!wayland\"\n          \"!fallback-x11\"\n          # \"x11\"\n        ];\n      };\n    };\n  };\n  xdg.portal = {\n    enable = true;\n    extraPortals = [\n      pkgs.xdg-desktop-portal-gtk\n    ];\n    config = {\n      common.default = [\"gtk\"];\n    };\n  };\n}\n</code></pre>\n<blockquote class=\"markdown-alert-note\">\n<p>I got the above configuration to build successfully, one of the hardening\nsteps I took isn’t allowing either app to launch. I’ll update once I find\nwhich setting it is exactly. (01-13-26)</p>\n</blockquote>\n</details>\n<ul>\n<li>\n<p><a href=\"https://docs.flathub.org/docs/for-users/verification\">Flathub Verified Apps</a></p>\n</li>\n<li>\n<p><a href=\"https://secureblue.dev/articles/flatpak\">Flatpak the good the bad the ugly</a></p>\n</li>\n</ul>\n<p>Then you can either find apps through <a href=\"https://flathub.org/en\">FlatHub</a> or on\nthe cmdline with <code>flatpak search &lt;app&gt;</code>. Flatpak is best used for GUI apps, some\nCLI apps can be installed through it but not all.</p>\n<ul>\n<li>\n<p>There is also <a href=\"https://github.com/gmodena/nix-flatpak\">nix-flatpak</a>, which\nenables you to manage your flatpaks declaratively.</p>\n</li>\n<li>\n<p><a href=\"https://flathub.org/en/apps/com.github.tchx84.Flatseal\">Flatseal</a> is GUI\nutility that enables you to review and modify permissions from your Flatpak\napps. Many apps by default come with smart-card support, X11 &amp; Wayland\nsupport, and more, disabling unnecessary permissions is recommended.</p>\n</li>\n<li>\n<p><a href=\"https://flathub.org/en/apps/io.github.flattool.Warehouse\">Warehouse</a> provides\na simple UI to control complex Flatpak options, no cmdline required.</p>\n</li>\n</ul>\n<p>I have heard that it is not recommended to use Flatpak browsers because in order\nfor flatpak to work it has to disable some of the built-in browser sandboxing\nwhich can reduce security. I haven’t found any examples of Flatpak browsers\nbeing exploited but it’s something to keep in mind.</p>\n<hr />\n<h2>SeLinux/AppArmor MAC (Mandatory Access Control)</h2>\n<p><strong>AppArmor</strong> is available on NixOS, but is still in a somewhat experimental and\nevolving state. There are only a few profiles that have been adapted to NixOS,\nsee here\n<a href=\"https://discourse.nixos.org/t/apparmor-default-profiles/16780\">Discourse on default-profiles</a>\nWhich guides you here\n<a href=\"https://github.com/NixOS/nixpkgs/blob/2acaef7a85356329f750819a0e7c3bb4a98c13fe/nixos/modules/security/apparmor/includes.nix\">apparmor/includes.nix</a>\nwhere you can see some of the abstractions and tunables to follow progress.</p>\n<p><strong>SELinux</strong>: Experimental, not fully integrated, recent progress for\nadvanced/curious users; expect rough edges and manual intervention if you want\nto try it. Most find SELinux more complex to configure and maintain than\nAppArmor.</p>\n<p>This isn’t meant to be a comprehensive guide, more to get people thinking about\nsecurity on NixOS.</p>\n<p>See the following guide on hardening networking:</p>\n<ul>\n<li><a href=\"https://saylesss88.github.io/nix/hardening_networking.html\">Hardening Networking</a></li>\n</ul>\n<hr />\n<h2>Resources</h2>\n<h3>Advanced Hardening with <code>nix-mineral</code> (Community Project)</h3>\n<details>\n<summary> ✔️ Click to Expand section on `nix-mineral` </summary>\n<p>For users seeking a more comprehensive and opinionated approach to system\nhardening beyond the built-in <code>hardened</code> profile, the community project\n<a href=\"https://github.com/cynicsketch/nix-mineral\"><code>nix-mineral</code></a> offers a declarative\nNixOS module.</p>\n<p><code>nix-mineral</code> aims to apply a wide array of security configurations, focusing on\ntweaking kernel parameters, system settings, and file permissions to reduce the\nattack surface.</p>\n<ul>\n<li><strong>Community Project Status:</strong> <code>nix-mineral</code> is a community-maintained project\nand is not officially part of the Nixpkgs repository or NixOS documentation.\nIts development status is explicitly stated as “Alpha software,” meaning it\nmay introduce stability issues or unexpected behavior.</li>\n</ul>\n<p>For detailed information on <code>nix-mineral</code>’s capabilities and current status,\nrefer directly to its\n<a href=\"https://github.com/cynicsketch/nix-mineral\">GitHub repository</a>.</p>\n</details>\n<details>\n<summary> ✔️ Click to Expand Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://hedgedoc.grimmauld.de/s/hWcvJEniW#\">AppArmor and apparmor.d on NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://tristanxr.com/post/selinux-on-nixos/\">SELinux on NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://xeiaso.net/blog/paranoid-nixos-2021-07-18/\">Paranoid NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Security\">NixOS Wiki Security</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/manual/nixos/unstable/index.html#sec-luks-file-systems\">Luks Encrypted File Systems</a></p>\n</li>\n<li>\n<p><a href=\"https://discourse.nixos.org/t/a-modern-and-secure-desktop-setup/41154\">Discourse A Modern and Secure Desktop</a></p>\n</li>\n<li>\n<p><a href=\"https://notashelf.dev/posts/insecurities-remedies-i\">notashelf NixOS Security 1 Systemd</a></p>\n</li>\n<li>\n<p><a href=\"https://ryanseipp.com/post/hardening-nixos/\">ryanseipp hardening-nixos</a></p>\n</li>\n<li>\n<p><a href=\"https://madaidans-insecurities.github.io/guides/linux-hardening.html\">madaidans Linux Hardening Guide</a></p>\n</li>\n<li>\n<p><a href=\"https://cybersecuritynews.com/hardening-linux-servers\">Hardening-Linux-Servers</a></p>\n</li>\n<li>\n<p><a href=\"https://linux-audit.com/linux-server-hardening-most-important-steps-to-secure-systems/\">linux-audit Linux Server hardening best practices</a></p>\n</li>\n<li>\n<p><a href=\"https://linux-audit.com/linux-security-guide-extended-version/\">linux-audit Linux security guide extended</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.archlinux.org/title/Security\">Arch Wiki Security</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.gentoo.org/wiki/Security_Handbook/Concepts\">Gentoo Security_Handbook Concepts</a></p>\n</li>\n<li>\n<p><a href=\"https://secureblue.dev/faq\">secureblue FAQ</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Documentation\">Excellent Kicksecure Docs</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/decalage2/awesome-security-hardening\">Awesome-Security-Hardening List</a></p>\n</li>\n<li>\n<p><a href=\"https://factorable.net/faq.html\">factorable.net (study of RSA and DSA crypto keys) FAQ</a></p>\n</li>\n<li>\n<p><a href=\"https://blog.cr.yp.to/20140205-entropy.html\">The cr.yp.to blog Entropy</a></p>\n</li>\n<li>\n<p><a href=\"https://delroth.net/posts/nixos-security-wishlist/\">NixOS Security wishlist</a></p>\n</li>\n<li>\n<p><a href=\"https://beej.us/guide/bgipc/html/\">Beejus IPC guide</a></p>\n</li>\n<li>\n<p><a href=\"https://www.geeksforgeeks.org/operating-systems/inter-process-communication-ipc/\">GeeksforGeeks IPC</a></p>\n</li>\n</ul>\n<p>neal.codes vulnerability scan script:</p>\n<pre><code class=\"language-bash\">nix-shell -p grype sbomnix --run '\n  sbomnix /run/current-system --csv /dev/null --spdx /dev/null --cdx sbom.cdx.json;\n  grype sbom.cdx.json\n'\n</code></pre>\n<ul>\n<li>\n<p><a href=\"https://github.com/nealfennimore/nixos-stig-anduril\">neal.codes nixos-stig-anduril</a></p>\n</li>\n<li>\n<p><a href=\"https://www.suse.com/c/linux-hardeningthe-complete-guide-to-securing-your-systems/\">Suse Linux Hardening Guide</a></p>\n</li>\n</ul>\n<p><strong>Government Resources 1st 6 come from gentoo’s Security_Handbook)</strong></p>\n<ul>\n<li>\n<p><a href=\"https://www.cyber.gov.au/sites/default/files/2023-03/Information%20Security%20Manual%20-%20%28March%202023%29.pdf\">The Austrailian Cyber Security Centre’s Informational Security Manual (ISM)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.protectivesecurity.gov.au/policies\">The Australian Government’s Protective Security Policy Framework (PSPF)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.cyber.gov.au/protect-yourself\">The Australian Cyber Security Centre’s Protect Yourself page</a></p>\n</li>\n<li>\n<p><a href=\"https://www.gov.uk/government/publications/security-policy-framework/hmg-security-policy-framework\">The UK Government’s Security Policy Framework (SPF)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.gov.uk/government/publications/information-security-policy-framework\">The UK Government’s Information Security Policy Framework (ISF)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.nist.gov/cybersecurity\">The US National Institute of Standards and Technology’s Cybersecurity page</a></p>\n</li>\n<li>\n<p><a href=\"https://stigviewer.com/stigs/anduril_nixos\">NixOS STIG</a></p>\n</li>\n<li>\n<p>STIGs are configuration standards developed by the Defense Information Systems\nAgency (DISA) to secure systems and software for the U.S. Department of\nDefense (DoD). They are considered a highly authoritative source for system\nhardening.There are recommendations for hardening all kinds of software in the\n<a href=\"https://stigviewer.com/stigs\">Stig Viewer</a></p>\n</li>\n<li>\n<p><a href=\"https://www.cisecurity.org/cis-benchmarks\">CIS Benchmarks</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nsacyber\">NSA Cybersecurity Directorate</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/alam00000/bentopdf\">bentopdf</a>: looks interesting, haven’t\nchecked it out yet though.</p>\n</li>\n<li>\n<p><a href=\"https://media.defense.gov/2023/Sep/08/2003296489/-1/-1/0/WHITFIELD%20DIFFIE_CRYPTOLOGIC%20ICONOCLAST.PDF\">Non-Secret Encryption a “SECRET” no longer</a></p>\n</li>\n<li>\n<p><a href=\"https://www.nsa.gov/Cybersecurity/Post-Quantum-Cybersecurity-Resources/\">Post-Quantum Cybersecurity Resources</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2026-06-17T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Understanding_Top-Level_Attributes_5.html",
      "url": "https://saylesss88.github.io/Understanding_Top-Level_Attributes_5.html",
      "title": "Top-Level Attributes",
      "content_html": "<h1>Chapter 5</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![coding1](images/coding1.png) -->\n<img src=\"https://saylesss88.github.io/images/gruv9.png\" width=\"800\" height=\"600\">\n<h2>Understanding Top-Level Attributes in NixOS Modules</h2>\n<p>This explanation is based on insights from Infinisil, a prominent figure in the\nNix community, to help clarify the concept of top-level attributes within NixOS\nmodules.</p>\n<blockquote>\n<p>[!NOTE] “top-level attributes” here refers to the attributes at the top level\nof a module file (imports, options, config), not to be confused with\n<code>system.build.toplevel</code>, which is the final system derivation everything\nbuilds toward.</p>\n</blockquote>\n<p>To understand why top-level module attributes matter, it helps to first\nunderstand what they’re ultimately building toward: <code>system.build.toplevel</code>, the\nfinal derivation that represents your entire NixOS system.</p>\n<hr />\n<h3>The Core of a NixOS System: <code>system.build.toplevel</code></h3>\n<details>\n<summary> ✔️ `system.build.toplevel` Explained (Click to Expand) </summary>\n<p>In a NixOS system, everything is built from a single “system derivation.” The\ncommand <code>nix-build '&lt;nixpkgs/nixos&gt;' -A system</code> initiates this build process.</p>\n<p>The <code>-A system</code> part tells Nix to focus on the <code>system</code> attribute defined in the\n<code>'&lt;nixpkgs/nixos&gt;'</code> file (which is essentially <code>./default.nix</code> within the\nNixpkgs repository).</p>\n<p>This <code>system</code> attribute is specifically the NixOS option <code>system.build.toplevel</code>\n. Think of <code>system.build.toplevel</code> as the <strong>very top of the configuration\nhierarchy</strong> for your entire NixOS system. Almost every setting you configure\neventually influences this top-level derivation, often through a series of\nintermediate steps.</p>\n</details>\n<h3>How Options Relate: A Chain of Influence</h3>\n<p>Options in NixOS are not isolated; they often build upon each other.</p>\n<details>\n<summary>Example: Nginx Option Chain (Click to Expand)</summary>\n<p>Here’s an example of how a high-level option can lead down to a low-level system\nconfiguration:</p>\n<ul>\n<li>You enable Nginx with <code>services.nginx.enable = true;</code>.</li>\n<li>This setting influences the lower-level option <code>systemd.services.nginx</code>.</li>\n<li>Which, in turn, affects the even lower-level option\n<code>systemd.units.\"nginx.service\"</code>.</li>\n<li>Ultimately, this leads to the creation of a systemd unit file within\n<code>environment.etc.\"systemd/system\"</code>.</li>\n<li>Finally, this unit file ends up as <code>result/etc/systemd/system/nginx.service</code>\nwithin the final <code>system.build.toplevel</code> derivation.</li>\n</ul>\n</details>\n<h3>The NixOS Module System: Evaluating Options</h3>\n<p>So, how do these options get processed and turned into the final system\nconfiguration? That’s the job of the <strong>NixOS module system</strong>, located in the\n<code>./lib</code> directory of Nixpkgs (specifically in <code>modules.nix</code>, <code>options.nix</code>, and\n<code>types.nix</code>).</p>\n<p>Interestingly, the module system isn’t exclusive to NixOS; you can use it to\nmanage option sets in your own Nix projects.</p>\n<p>Here’s a simplified example of using the module system outside of NixOS:</p>\n<pre><code class=\"language-nix\">let\n  systemModule = { lib, config, ... }: {\n    options.toplevel = lib.mkOption {\n      type = lib.types.str;\n    };\n\n    options.enableFoo = lib.mkOption {\n      type = lib.types.bool;\n      default = false;\n    };\n\n    config.toplevel = ''\n      Is foo enabled? ${lib.boolToString config.enableFoo}\n    '';\n  };\n\n  userModule = {\n    enableFoo = true;\n  };\n\nin (import &lt;nixpkgs/lib&gt;).evalModules {\n  modules = [ systemModule userModule ];\n}\n</code></pre>\n<p><strong>You can evaluate the <code>config.toplevel</code> option from this example using:</strong></p>\n<pre><code class=\"language-bash\">nix-instantiate --eval file.nix -A config.toplevel\n</code></pre>\n<hr />\n<h3>How the Module System Works: A Simplified Overview</h3>\n<p>The module system processes a set of “modules” through these general steps:</p>\n<ol>\n<li>\n<p><strong>Importing Modules</strong>: It recursively finds and includes all modules\nspecified in <code>imports = [ ... ];</code> statements.</p>\n</li>\n<li>\n<p><strong>Declaring Options</strong>: It collects all option declarations defined using\n<code>options = { ... };</code> from all the modules and merges them. If the same option\nis declared in multiple modules, the module system handles this (details\nomitted for simplicity).</p>\n</li>\n<li>\n<p><strong>Defining Option Values</strong>: For each declared option, it gathers all the\nvalue assignments (defined using <code>config = { ... };</code> or directly at the top\nlevel if no <code>options</code> or <code>config</code> are present) from all modules and merges\nthem according to the option’s defined type.</p>\n</li>\n</ol>\n<blockquote>\n<p>[!NOTE] Option evaluation is lazy, meaning an option’s value is only computed\nwhen it’s actually needed. It can also depend on the values of other options.</p>\n</blockquote>\n<hr />\n<p><strong>Top-Level Attributes in a Module: <code>imports</code>, <code>options</code>, and <code>config</code></strong></p>\n<p>Within a NixOS module (the files that define parts of your system configuration)\n, the attributes defined directly at the top level of the module’s function have\nspecific meanings:</p>\n<ul>\n<li>\n<p><code>imports</code>: This attribute is a list of other module files to include. Their\noptions and configurations will also be part of the evaluation.</p>\n</li>\n<li>\n<p><code>options</code>: This attribute is where you declare new configuration options. You\ndefine their type, default value, description, etc., using functions like\n<code>lib.mkOption</code> or <code>lib.mkEnableOption</code>.</p>\n</li>\n<li>\n<p><code>config</code>: This attribute is where you assign values to the options that have\nbeen declared (either in the current module or in imported modules).</p>\n</li>\n</ul>\n<p><strong>The Rule: Move Non-Option Attributes Under <code>config</code></strong></p>\n<p>If you define either an <code>options</code> or a <code>config</code> attribute at the top level of\nyour module, any other attributes that are not option declarations must be moved\ninside the config attribute.</p>\n<details>\n<summary> ✔️ Examples of Correct and Incorrect Usage (Click to Expand)</summary>\n<p>Let’s look at an example of what not to do:</p>\n<pre><code class=\"language-nix\">{ pkgs, lib, config, ... }:\n{\nimports = [];\n\n# Defining an option at the top level\n\noptions.mine.desktop.enable = lib.mkEnableOption \"desktop settings\";\n\n# This will cause an error because 'environment' and 'appstream'\n\n# are not 'options' and 'config' is also present at the top level.\n\nenvironment.systemPackages =\nlib.mkIf config.appstream.enable [ pkgs.git ];\n\nappstream.enable = true;\n}\n</code></pre>\n<p>This will result in the error:\n<code>error: Module has an unsupported attribute 'appstream' This is caused by introducing a top-level 'config' or 'options' attribute. Add configuration attributes immediately on the top level instead, or move all of them into the explicit 'config' attribute</code>.</p>\n<p><strong>The Correct Way</strong>: Using the <code>config</code> Attribute</p>\n<p>To fix the previous example, you need to move the value assignments for\n<code>environment.systemPackages</code> and <code>appstream.enable</code> inside the config attribute:</p>\n<pre><code class=\"language-nix\">{ pkgs, lib, config, ... }:\n{\nimports = [];\n\n# Defining an option at the top level\n\noptions.mine.desktop.enable = lib.mkEnableOption \"desktop settings\";\n\nconfig = {\nenvironment.systemPackages =\nlib.mkIf config.appstream.enable [ pkgs.git ];\n\n    appstream.enable = true;\n\n};\n}\n</code></pre>\n<p>Now, Nix knows that you are declaring an option (<code>options.mine.desktop.enable</code>)\nand then setting values for other options (<code>environment.systemPackages</code>,\n<code>appstream.enable</code>) within the <code>config</code> block.</p>\n<p><strong>Implicit <code>config</code>: When <code>options</code> is Absent</strong></p>\n<p>If your module does not define either <code>options</code> or <code>config</code> at the top level,\nthen any attributes you define directly at the top level are implicitly treated\nas being part of the config.</p>\n<p>For example, this is valid:</p>\n<pre><code class=\"language-nix\">{ pkgs, lib, config, ... }:\n{\nenvironment.systemPackages =\nlib.mkIf config.appstream.enable [ pkgs.git ];\n\nappstream.enable = true;\n}\n</code></pre>\n<p>Nix will implicitly understand that <code>environment.systemPackages</code> and\n<code>appstream.enable</code> are configuration settings.</p>\n<p><strong>Removing an Option: What Happens to <code>config</code></strong></p>\n<p>Even if you remove the <code>options</code> declaration from a module that has a <code>config</code>\nsection, the <code>config = { environment.systemPackages = ... };</code> part will still\nfunction correctly, assuming the option it’s referencing (<code>appstream.enable</code> in\nthis case) is defined elsewhere (e.g., in an imported module).</p>\n</details>\n<h4>Conclusion</h4>\n<p>Understanding the nuances of top-level attributes within NixOS modules,\nparticularly <code>imports</code>, <code>options</code>, and <code>config</code>, is fundamental to structuring\nand managing your system’s configuration effectively. As we’ve seen, the module\nsystem provides a powerful and declarative way to define and evaluate system\nsettings, ultimately contributing to the construction of the\n<code>system.build.toplevel</code> derivation that represents your entire NixOS\nenvironment.</p>\n<p>The concepts of option declaration and value assignment, along with the crucial\nrule of organizing non-option attributes under the <code>config</code> attribute when\n<code>options</code> is present, provide a clear framework for building modular and\nmaintainable configurations.</p>\n<p>Now that we have a solid grasp of how NixOS modules are structured and how they\ncontribute to the final system derivation, it’s a natural next step to explore\nthe tangible results of these configurations: the software and system components\nthemselves. These are built and managed by a core concept in Nix, known as\n<strong>derivations</strong>.</p>\n<p>In the next chapter,\n<a href=\"https://saylesss88.github.io/Package_Definitions_Explained_6.html\">Package Definitions Explained</a>\nwe will shift our focus from the abstract configuration to the concrete software\npackages. We will learn how Nix uses <em>package definitions</em> to create\n<em>derivations</em>, which are the actual build plans that produce the software we use\non our NixOS systems. This will bridge the gap between configuring your system\nand understanding how the software within it is managed.</p>\n",
      "date_published": "2026-05-31T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/vcs/practical_jj.html",
      "url": "https://saylesss88.github.io/vcs/practical_jj.html",
      "title": "Practical Jujutsu",
      "content_html": "<h1>Practical Jujutsu</h1>\n<p>This post assumes basic understanding of Git and GitHub.</p>\n<p>I’ve spent enough time hovering between the familiarity of Git and the potential\nof Jujutsu. It’s time to move past a ‘primitive’ workflow. By truly mastering\none of these tools, I want to turn version control from a chore into a way to\nprecisely navigate my development stages and build a history that future\ncontributors can actually follow.</p>\n<p>JJ simplifies keeping a linear history and makes it easy to break down big\nchanges into smaller atomic changes.</p>\n<details>\n<summary> Atomic commits & Linear History explained </summary>\n<blockquote>\n<ul>\n<li><a href=\"https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md\">Angular Commit Message Format</a></li>\n</ul>\n<pre><code class=\"language-text\">&lt;header&gt;\n&lt;BLANK LINE&gt;\n&lt;body&gt;\n&lt;BLANK LINE&gt;\n&lt;footer&gt;\n</code></pre>\n<ul>\n<li>The <code>header</code> is mandatory.</li>\n<li>The <code>body</code> is mandatory for all commits except “doc” type commits.</li>\n<li>The <code>footer</code> is optional</li>\n</ul>\n</blockquote>\n<ol>\n<li>Atomic Commits</li>\n</ol>\n<ul>\n<li>\n<p>An atomic commit is a single unit of work that cannot be broken down further\nwithout losing its meaning.</p>\n</li>\n<li>\n<p>One commit should do one thing.</p>\n</li>\n<li>\n<p>If you had to “undo” that commit later, would it break unrelated features? If\n“Yes,” it’s not atomic.</p>\n</li>\n</ul>\n<p>If you find a bug, you can pinpoint the exact 10 lines of code that caused it.\nIn <code>jj</code>, the <code>split -i</code> and <code>commit -i</code> commands are the ultimate tools for\n“atomizing” a messy afternoon of coding.</p>\n<ol start=\"2\">\n<li>Linear History</li>\n</ol>\n<p>A linear history is a straight line of commits without “merge bubbles” (those\ncriss-crossing lines you see in Git logs when people use git merge).</p>\n<ul>\n<li>\n<p>Every commit has exactly one parent and one child.</p>\n</li>\n<li>\n<p>It reads like a story. You can follow the evolution of the project from bottom\nto top without getting lost in a maze of branches.</p>\n</li>\n<li>\n<p><code>jj</code> defaults to a rebase-heavy workflow. Instead of “merging” your work and\ncreating a mess, you are constantly “sliding” your changes on top of the\nlatest work, keeping that line perfectly straight.</p>\n</li>\n</ul>\n<p>You can even insert a commit anywhere in your history with <code>jj new -A</code>\n(<code>--insert-after</code>), and <code>jj new -B</code> (<code>--insert-before</code>) and JJ will rebase every\nchild in the history.</p>\n<p>Use <code>jj show -r &lt;revision&gt;</code> to see a diff of the changes made at that Change ID,\nand trivially craft it with <code>jj edit -r &lt;revision&gt;</code>.</p>\n</details>\n<p>Let’s learn about <code>jj</code> by using it to version control a system Nix Flake.</p>\n<h2>Quick Overview</h2>\n<ul>\n<li><a href=\"https://docs.jj-vcs.dev/latest/glossary/\">Jujutsu docs Glossary</a></li>\n</ul>\n<details>\n<summary>Key Terms</summary>\n<blockquote>\n<p>“One of the first things to wrap your head around when first coming to Jujutsu\nis its approach to its revisions and revsets, i.e. “sets of revision”.\nRevisions are the fundamental elements of changes in Jujutsu, not “commits” as\nin Git. Revsets are then expressions in a functional language for selecting a\nset of revisions.”\n–<a href=\"https://v5.chriskrycho.com/essays/jj-init/\">Chris Krycho jj init</a></p>\n</blockquote>\n<table><thead><tr><th>Term</th><th>What it is</th><th>Git Equivalent</th><th>JJ Behavior</th></tr></thead><tbody>\n<tr><td><strong>Working Copy (<code>@</code>)</strong></td><td>Your current editable commit. <strong>Everything you do affects <code>@</code> by default.</strong> Auto‑amends as you save files.</td><td>Untracked files + staging area + HEAD</td><td>Always a full commit. No staging. <code>jj st</code> shows changes relative to <code>@-</code>.</td></tr>\n<tr><td><strong>Change ID</strong></td><td>Stable ID for a logical unit of work (the <code>k</code>, <code>y</code> labels in logs). Survives edits/rebases.</td><td>N/A</td><td>Prefix like <code>k</code> or <code>y</code>. Use <code>jj edit k1234</code> to jump to any change.</td></tr>\n<tr><td><strong>Commit ID</strong></td><td>Unique ID for a specific snapshot (the long hex like <code>41deb985</code>). Changes when you amend.</td><td>Commit hash</td><td>Full ID for exact snapshots. Rarely used directly.</td></tr>\n<tr><td><strong>Bookmarks</strong></td><td>Named pointers to commits (like <code>main</code>, <code>feature-x</code>). <strong>Don’t auto‑move</strong> like Git branches.</td><td>Branches</td><td><code>jj bookmark set main -r @</code> moves it. <code>*</code> shows if local/remote match.</td></tr>\n<tr><td><strong>Parent (<code>@-</code>)</strong></td><td>The commit <code>@</code> is built on top of.</td><td>Previous commit</td><td>Use <code>-r @-</code> to target it. Key for squash workflow.</td></tr>\n<tr><td><strong>Immutable (<code>◆</code>)</strong></td><td>Commits that shouldn’t be rewritten (pushed changes, trunk).</td><td>Protected branches</td><td><code>jj log</code> shows <code>◆</code>. Still editable with force flags.</td></tr>\n<tr><td><strong>Revset</strong></td><td>Query language for commits (<code>main..@</code>, <code>mine()</code>).</td><td><code>git log --grep</code></td><td>Super powerful. <code>jj log -r \"main..@\"</code> = changes since main.</td></tr>\n<tr><td><strong>Revision</strong></td><td>“revision” is synonymous with “commit”</td><td>Commit</td><td>A synonym for Commit</td></tr>\n</tbody></table>\n<p><strong>Pro tip</strong>: <code>@</code> is <strong>always</strong> your current position. <code>jj new</code>, <code>jj desc</code>,\n<code>jj squash</code> all default to it. <strong>Bookmarks like <code>main</code> are just labels</strong> - move\nthem explicitly with <code>jj bookmark set</code>.</p>\n</details>\n<p>See\n<a href=\"https://zerowidth.com/2025/jj-tips-and-tricks/\">zerowidths jj-tips-and-tricks</a>,\nfor a more intuitive <code>--interactive</code> workflow (The <code>git add -p</code> Hunk-wise\nstyle).</p>\n<h2>Getting Started</h2>\n<p>It’s helpful to grasp a few Git concepts to fully understand some of the\nbenefits and strengths of jujutsu. I highly suggest reading\n<a href=\"https://www.w3tutorials.net/blog/how-to-compare-the-working-copy-staging-copy-and-committed-copy-of-a-file-using-git/\">W3Tutorials.net How to Compare Working Copy, Staging Copy, and Committed Copy of a File in Git</a>,\nit covers most of the concepts that make understanding <code>jj</code> easier.</p>\n<ul>\n<li>\n<p>The <strong>Working Copy</strong> is the version of the file you’re actively editing in\nyour filesystem. It’s the “live” version you see in your text editor or IDE.\n–W3Tutorials.net</p>\n</li>\n<li>\n<p><a href=\"https://docs.jj-vcs.dev/latest/working-copy/\">Jujutsu’s Working copy commit</a></p>\n</li>\n</ul>\n<blockquote>\n<p>“Unlike most other VCSs, Jujutsu will automatically create commits from the\nworking-copy contents when they have changed. Most <code>jj</code> commands you run will\ncommit the working-copy changes if they have changed. The resulting revision\nwill replace the previous working-copy revision.”</p>\n</blockquote>\n<p>In Git, <code>HEAD</code> is a pointer (usually to a branch like <code>main</code>), and that branch\npoints to the current commit.</p>\n<p>In jj, the working copy <code>@</code> is itself the current commit. Bookmarks like <code>main</code>\nsimply point at other commits in the graph, so your working copy is often a\nchild of a bookmark, but it can also be off on its own.</p>\n<ul>\n<li><code>❯</code> will indicate a command that I ran, the rest is output.</li>\n</ul>\n<p>Let’s start by cloning a project of mine to see how <code>jj</code> works:</p>\n<pre><code class=\"language-bash\">❯ jj git clone git@github.com:sayls8/nix-snake.git\nFetching into new repo in \"/home/jr/projects/nix-snake\"\nremote: Enumerating objects: 88, done.\nremote: Total 88 (delta 38), reused 78 (delta 30), pack-reused 0 (from 0)\nbookmark: main@origin [new] tracked\nSetting the revset alias `trunk()` to `main@origin`\nWorking copy  (@) now at: s f143a1da (empty) (no description set)\nParent commit (@-)      : l 3b759dfe main | feat: fix clippy lints &amp; optimize\nAdded 12 files, modified 0 files, removed 0 files\nHint: Running `git clean -xdf` will remove `.jj/`!\n</code></pre>\n<ul>\n<li>We can see above that the <code>main@origin</code> bookmark is automatically tracked with\na revset alias of <code>trunk()</code>.</li>\n</ul>\n<blockquote>\n<p>NOTE: My jj commands show the shortest possible IDs because of the setting:</p>\n<pre><code class=\"language-nix\">programs.jujutsu.settings = {\n   template-aliases = {\n       \"format_short_change_id(id)\" = \"id.shortest()\";\n   };\n};\n</code></pre>\n</blockquote>\n<p>Running <code>jj st</code> &amp; <code>jj log</code>:</p>\n<pre><code class=\"language-bash\">❯  jj st\nThe working copy has no changes.\nWorking copy  (@) : s f143a1da (empty) (no description set)\nParent commit (@-): l 3b759dfe main | feat: fix clippy lints &amp; optimize\n\n❯  jj log\n@  s sayls8@proton.me 2026-03-21 16:31:59 f143a1da\n│  (empty) (no description set)\n◆  l sayls8@proton.me 2026-01-29 16:50:51 main 3b759dfe\n│  feat: fix clippy lints &amp; optimize\n~\n</code></pre>\n<p>To show all ancestors of the most recent commit, <code>s</code> in this case:</p>\n<pre><code class=\"language-bash\">jj log -r ::s\n</code></pre>\n<ul>\n<li>\n<p>The <code>@</code> indicates the working-copy commit. The first ID on a line (e.g. “s”\nabove) is the change ID. The second ID is the commit ID (“f143a1da”). You can\ngive either ID to commands that take revisions as arguments.</p>\n</li>\n<li>\n<p><code>jj log</code> defaults to the <code>ui.default-revset</code> setting, or\n<code>@ | ancestors(immutable_heads().., 2) | heads(immutable_heads())</code> if it’s not\nset. (A revset)</p>\n</li>\n<li>\n<p><code>jj</code> commands default to operating on the working copy, <code>@</code>. <code>jj undo</code>, undoes\nyour previous <code>jj</code> command. List all previous <code>jj</code> commands with <code>jj op log</code>.</p>\n</li>\n</ul>\n<p>In JJ, you are never “on” a branch. You are always “on” a specific change,\nbuilding a stack of changes. Until you push those changes, you can continue to\njump around and edit them with <code>jj edit</code>. Once you push those changes to a\nremote, they then become immutable.</p>\n<p>You don’t have to merge <code>Change A</code> into <code>Change B</code>, because <code>Change B</code> is\nalready built on top of <code>Change A</code>. It inherits every line of code from the\nfloors below it.</p>\n<p>In Git, the working copy is the files on disk; changes only become part of the\nnext commit when you stage them in the index with git add. In Jujutsu, the\nworking copy is the current commit: your edits live in a “working copy commit”\n(<code>@</code>), which <code>jj</code> automatically updates from the files on disk, and there is no\nseparate staging area.</p>\n<p>When you’re ready to push to GitHub, make sure you know where your changes are.\nIn the example above, the working copy is empty, so to push I’d run\n<code>jj bookmark set main -r @-</code> to point the <code>main</code> bookmark at the latest changes,\nthen <code>jj git push</code>.</p>\n<ul>\n<li><code>jj git push</code>: By default, pushes tracking bookmarks pointing to\n<code>remote_bookmarks(remote=&lt;remote&gt;)..@</code>. Use <code>--bookmark</code> to push specific\nbookmarks. Use <code>--all</code> to push all bookmarks. Use <code>--change</code> to generate\nbookmark names based on the change IDs of specific commits.</li>\n</ul>\n<p>If the working copy isn’t empty and those changes are what I want to push, I’d\ninstead run <code>jj bookmark set main -r @</code>, followed by <code>jj git push</code>. Once you\nunderstand this distinction, the rest of the workflow feels fairly intuitive.</p>\n<ul>\n<li>\n<p><code>jj</code> is smart enough to know that: If <code>main</code> is on your Working Copy (<code>@</code>) and\nyou have uncommitted changes, it pushes those.</p>\n</li>\n<li>\n<p>If your Working Copy (<code>@</code>) is empty , and <code>main</code> is on the Parent (<code>@-</code>), it\npushes the parent.</p>\n</li>\n</ul>\n<p>In the above example, if I remove the <code>config</code> argument to the\n<code>configuration.nix</code> and remove a few comments, then run <code>jj diff</code>:</p>\n<p><img src=\"https://saylesss88.github.io/../images/jj-diff.png\" alt=\"jj diff\" /></p>\n<ul>\n<li>\n<p>The <code>squash</code> workflow would benefit especially from <code>jj diff</code>. It’s like\nsaying “take the diff I’m looking at right now and bake it directly into the\nparent”.</p>\n</li>\n<li>\n<p>A <strong>change</strong> is a commit that can evolve while keeping a stable identifier,\nthe <strong>change ID</strong>.</p>\n</li>\n</ul>\n<hr />\n<h2>Version Control Best Practices</h2>\n<p>It’s helpful to use\n<a href=\"https://www.conventionalcommits.org/en/v1.0.0/\">Conventional Commits</a>, a set of\nrules for creating an explicit commit history.</p>\n<p>Commit message standard syntax:</p>\n<pre><code class=\"language-text\">&lt;type&gt;[optional scope]: &lt;description&gt;\n\n[optional body]\n\n[optional footer(s)]\n</code></pre>\n<p>Example:</p>\n<pre><code class=\"language-bash\">feat: implement dendretic pattern for boot module\n</code></pre>\n<p><strong>Useful Utils</strong></p>\n<ul>\n<li>\n<p>There is a new project that helps you create conventional commits for jj,\n<a href=\"https://crates.io/crates/jj-commit\">jj-commit</a>.</p>\n</li>\n<li>\n<p><a href=\"https://crates.io/crates/commitlint-rs\">commitlint-rs</a> can be used as a Git\nhook on push to enforce conventional commits.</p>\n</li>\n<li>\n<p><a href=\"https://github.com/semantic-release/semantic-release\">semantic-release</a>\nautomates the whole package release workflow.</p>\n</li>\n</ul>\n<p><strong>Using Tags</strong></p>\n<p>Tags are just named pointers to commits. Create one pointing to your latest\nchange:</p>\n<pre><code class=\"language-bash\">jj st\nThe working copy has no changes.\nWorking copy  (@) : ysrlmzlt 7d093162 (empty) (no description set)\nParent commit (@-): wmuwoply ea03805e chore: add 'release version' to justfile\n</code></pre>\n<pre><code class=\"language-bash\">jj tag set v0.1.5 --revision @-\njj git push --tags\ncargo publish\n</code></pre>\n<p>When you publish next, create a new tag <code>v0.1.6</code>.</p>\n<p>The simple order:</p>\n<ol>\n<li>\n<p>Write code, commit, <code>jj git push</code> as normal (many times)</p>\n</li>\n<li>\n<p>When ready to release:</p>\n</li>\n</ol>\n<ul>\n<li>bump version in <code>Cargo.toml</code> to <code>0.1.6</code></li>\n<li>update CHANGELOG</li>\n<li>commit</li>\n<li><code>jj git push</code></li>\n<li><code>jj tag set v0.1.6 --revision @-</code></li>\n<li><code>jj git push --tag v0.1.6</code> # This only pushes the tag, not the commit again</li>\n<li><code>cargo publish</code></li>\n</ul>\n<p>That’s it. Tags only appear at step 2, once per release.</p>\n<p><strong>One tag per release, on the commit you publish. Everything in between is just\nnormal commits with no tags involved.</strong></p>\n<p>You can also use <code>-dev</code> tags between releases. Right after the release, set your\nversion in your <code>Cargo.toml</code> to <code>0.1.7-dev</code> and that will make clear which\ncommit’s haven’t been published yet.</p>\n<h3>The edit workflow</h3>\n<p>Initialize and colocate the repository:</p>\n<pre><code class=\"language-bash\">❯  mkdir learn-jj\n\n  ~/projects\n❯  cd learn-jj\n\n\n  ~/projects/learn-jj\n❯  nix flake new . -t github:nix-community/home-manager#nixos\nwrote: \"/home/jr/projects/learn-jj/flake.nix\"\n\n  ~/projects/learn-jj  ✗\n❯  jj git init --colocate\nInitialized repo in \".\"\nHint: Running `git clean -xdf` will remove `.jj/`!\n\n  learn-jj   main [?]\n❯  jj git remote add origin git@github.com:sayls8/learn-jj.git\n\n  learn-j   main [?]\n❯  jj bookmark create main -r @\nDone importing changes from the underlying Git repo.\nCreated 1 bookmarks pointing to l bd3847c0 main | (no description set)\n\n  learn-jj   refs/jj/root [!]\n❯  jj bookmark track main --remote=origin\nStarted tracking 1 remote bookmarks.\n</code></pre>\n<p>Let’s give it our current change a description:</p>\n<pre><code class=\"language-bash\">❯  jj desc -m \"chore: Initialize system flake\"\nWorking copy  (@) now at: l 743b170d main* | chore: Initialize system flake\nParent commit (@-)      : z 00000000 (empty) (no description set)\n</code></pre>\n<p>In this example, the Parent commit is the <em>root commit</em>. The root commit is a\nvirtual commit at the root of every repository. It has a commit ID consisting of\nall ’0’s (<code>00000000...</code>) and a change ID consisting of all ’z’s (<code>zzzzzzzz...</code>).\nIt can be referred to in revsets by the function <code>root()</code>.</p>\n<ul>\n<li>\n<p>With this workflow, your working copy is typically at your current change.</p>\n</li>\n<li>\n<p>JJ treats the working copy as a commit rather than having an index like Git.</p>\n</li>\n</ul>\n<p>If we wanted to push right now we could with <code>jj git push</code> (or equivalently\n<code>jj git push --bookmark main</code>), but let’s first learn a bit more about how <code>jj</code>\nworks.</p>\n<p>Let’s say we’re done with the current change and we’re ready to make it\nimmutable and start a new change:</p>\n<pre><code class=\"language-bash\">❯ jj new -m \"chore: change username &amp; hostname in flake.nix\"\nWorking copy  (@) now at: p 6524f35a (empty) chore: change username &amp; hostname in flake.nix\nParent commit (@-)      : l 743b170d main* | chore: Initialize system flake\n</code></pre>\n<ul>\n<li>\n<p>As stated above, <code>jj</code> commands default to the working copy. So <code>jj new</code> is the\nsame as <code>jj new -r @</code>. By running <code>jj new</code> repeatedly, we build a linear stack\nwhere each change is a child of the previous one. When we’re ready to ‘check\nin’ our work, we don’t merge; we simply move the <code>main</code> bookmark to our\ncurrent position and push.</p>\n<ul>\n<li>If we want to start a separate task without including our current work, we\ncan run <code>jj new main</code> (or any other Change ID). This creates a sibling\nchange. Our previous stack isn’t “lost”; it stays exactly where it was in\nthe graph, waiting to be described, rebased, or merged later.</li>\n</ul>\n</li>\n<li>\n<p>Now our Working copy <code>@</code> is at an <code>(empty)</code> change with the description\n“chore: change username &amp; hostname in flake.nix”.</p>\n</li>\n<li>\n<p>As you can see, running <code>jj new</code>, does not move the <code>main</code> bookmark. This is\nthe hardest part to grasp when coming from Git IMO. Let’s make some more\nchanges to hammer this home.</p>\n</li>\n</ul>\n<p>I’ve added my hostname and username to the <code>flake.nix</code> template, let’s make them\na part of the permanent record.</p>\n<p>I create a minimal <code>configuration.nix</code>, check my status and notice that I forgot\nto run <code>jj new -m \"feat: create minimal configuration.nix\"</code>. Let’s see how to\nrecover from this and keep our commits atomic.</p>\n<pre><code class=\"language-bash\">jj split -i\n</code></pre>\n<ul>\n<li>This opens up a diff editor, I’ll only press <code>y</code> for the changes related to\nusername and hostname. After you pass on what you don’t want in this change\nand press <code>y</code> on what you do want, your $EDITOR will open with your previous\ncommit message. Save it and another commit message will open up in $EDITOR,\nthis is whatever you didn’t press <code>y</code> on i.e., the <code>configuration.nix</code>\nchanges, just give the second set of changes a different description and\nyou’re all set.</li>\n</ul>\n<p>Another cool thing about <code>jj</code> is that you can add a description whenever you\nwant. Running <code>jj desc -m \"add configuration.nix\"</code> doesn’t finalize your commit\nlike it does with Git. So, you can put the description first, last, or in the\nmiddle of a current change with no issue. The equivalent command to\n<code>git commit -m \"message\"</code> is <code>jj commit -m \"message\" &amp;&amp; jj new</code></p>\n<p>Let’s see what the <code>jj split -i</code> command left us with:</p>\n<pre><code class=\"language-bash\">❯  jj\nWorking copy changes:\nA configuration.nix\nWorking copy  (@) : m b3cc09db feat: create minimal configuration.nix\nParent commit (@-): p cddde3b4 chore: change username &amp; hostname in flake.nix\n</code></pre>\n<ul>\n<li>To see a diff of the changes in the parent commit:</li>\n</ul>\n<pre><code class=\"language-bash\">jj show -r @-\n</code></pre>\n<p>And our <code>log</code>:</p>\n<pre><code class=\"language-bash\">❯  jj log\n@  m sayles8@proton.me 2026-03-15 13:43:31 b3cc09db\n│  feat: create minimal configuration.nix\n○  p sayls8@proton.me 2026-03-15 13:41:19 cddde3b4\n│  chore: change username &amp; hostname in flake.nix\n○  l sayls8@proton.me 2026-03-15 13:37:29 main* 743b170d\n│  chore: Initialize system flake\n◆  z root() 00000000\n</code></pre>\n<p>As you can see, <code>main*</code> is all the way back at change <code>l</code>. Let’s move our <code>main</code>\nbookmark to our current Working copy.</p>\n<pre><code class=\"language-bash\">❯  jj bookmark set main -r @\nMoved 1 bookmarks to m b3cc09db main* | feat: create minimal configuration.nix\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj log\n@  m sayls8@proton.me 2026-03-15 13:43:31 main* b3cc09db\n│  feat: create minimal configuration.nix\n○  p sayls8@proton.me 2026-03-15 13:41:19 cddde3b4\n│  chore: change username &amp; hostname in flake.nix\n○  l sayls8@proton.me 2026-03-15 13:37:29 743b170d\n│  chore: Initialize system flake\n◆  z root() 00000000\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj git push\nChanges to push to origin:\n  Add bookmark main to b3cc09dba32c\ngit: Enumerating objects: 9, done.\ngit: Counting objects: 100% (9/9), done.\ngit: Delta compression using up to 16 threads\ngit: Compressing objects: 100% (7/7), done.\ngit: Writing objects: 100% (9/9), 1.99 KiB | 1019.00 KiB/s, done.\ngit: Total 9 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)\nremote: Resolving deltas: 100% (1/1), done.\nWarning: The working-copy commit in workspace 'default' became immutable, so a new commit has been created on top of it.\nWorking copy  (@) now at: u 551e83ad (empty) (no description set)\nParent commit (@-)      : m b3cc09db main | feat: create minimal configuration.nix\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj log\n@  u sayls8@proton.me 2026-03-15 13:47:27 551e83ad\n│  (empty) (no description set)\n◆  m sayls8@proton.me 2026-03-15 13:43:31 main b3cc09db\n│  feat: create minimal configuration.nix\n</code></pre>\n<ul>\n<li>\n<p>Notice <code>jj log</code> now shows <code>main</code> instead of <code>main*</code>, indicating that <code>main</code>\nand <code>origin@main</code> are in sync!</p>\n</li>\n<li>\n<p>Also notice the <code>◆</code> next to the <code>m</code> change, this indicates that this change is\nnow immutable. This is mentioned in the output of <code>jj git push</code> above.</p>\n<ul>\n<li><code>jj</code> does this so you don’t accidentally rewrite history that others might\nhave pulled. You can still force-edit if you need to but it’s <code>jj</code>s way of\nsaying, “This is now part of the public record”.</li>\n</ul>\n</li>\n</ul>\n<p>I now need to add a minimal <code>home.nix</code>, then run <code>nix flake check</code> to see if I\nforgot anything.</p>\n<p>If something isn’t being picked up by <code>jj</code> try running <code>jj st</code> and check again.\nRunning any <code>jj</code> command updates the Working copy.</p>\n<p>Since when running <code>jj git push</code> <code>jj</code> automatically creates a new commit on top\nof the last one, the next step is to describe this change.</p>\n<p>I ran <code>nix flake check</code> and needed to add a <code>hardware-configuration.nix</code>, and\n<code>networking.hostId</code> required by ZFS, if I wanted to be a stickler about atomic\ncommits I’d run <code>jj split -i</code> again but it’s fine by me to make 2 small changes\nto get the flake to pass the <code>check</code>.</p>\n<h2>The squash Workflow</h2>\n<p>The last section left me with:</p>\n<pre><code class=\"language-bash\">❯  jj st\nWorking copy changes:\nM configuration.nix\nA flake.lock\nA hardware-configuration.nix\nA home.nix\nWorking copy  (@) : u e57a7a39 feat: add minimal home.nix\nParent commit (@-): m b3cc09db main | feat: create minimal configuration.nix\n</code></pre>\n<p>Let’s push what we have:</p>\n<pre><code class=\"language-bash\">❯  jj bookmark set main -r @\nMoved 1 bookmarks to u e57a7a39 main* | feat: add minimal home.nix\n\n  learn-jj   HEAD [!]\n❯  jj git push\nChanges to push to origin:\n  Move forward bookmark main from b3cc09dba32c to e57a7a39957a\ngit: Enumerating objects: 8, done.\ngit: Counting objects: 100% (8/8), done.\ngit: Delta compression using up to 16 threads\ngit: Compressing objects: 100% (6/6), done.\ngit: Writing objects: 100% (6/6), 1.98 KiB | 1.98 MiB/s, done.\ngit: Total 6 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)\nremote: Resolving deltas: 100% (1/1), completed with 1 local object.\nWarning: The working-copy commit in workspace 'default' became immutable, so a new commit has been created on top of it.\nWorking copy  (@) now at: y 53e8a3d9 (empty) (no description set)\nParent commit (@-)      : u e57a7a39 main | feat: add minimal home.nix\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj st\nThe working copy has no changes.\nWorking copy  (@) : y 53e8a3d9 (empty) (no description set)\nParent commit (@-): u e57a7a39 main | feat: add minimal home.nix\n</code></pre>\n<p>Great, just what we need, an empty change! Let’s describe what we plan on doing:</p>\n<pre><code class=\"language-bash\">jj desc -m \"refactor: restructure flake to multi-host layout in hosts/magic\"\n</code></pre>\n<p>Now we create a new change on top of this one:</p>\n<pre><code class=\"language-bash\">❯  jj new\nWorking copy  (@) now at: w 43195106 (empty) (no description set)\nParent commit (@-)      : y c66bc991 (empty) refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<p>Now we make our changes to the descriptionless Working copy and <code>squash</code> our\nchanges into the parent commit.</p>\n<pre><code class=\"language-bash\">mkdir -p hosts/magic\n</code></pre>\n<pre><code class=\"language-bash\">jj st\nThe working copy has no changes.\nWorking copy  (@) : w 43195106 (empty) (no description set)\nParent commit (@-): y c66bc991 (empty) refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<p>Ahh, <code>jj</code> doesn’t pick up empty directories…</p>\n<pre><code class=\"language-bash\">mv configuration.nix home.nix hosts/magic\n</code></pre>\n<pre><code class=\"language-bash\"> jj st\nWorking copy changes:\nR {configuration.nix =&gt; hosts/magic/configuration.nix}\nR {home.nix =&gt; hosts/magic/home.nix}\nWorking copy  (@) : w 8c49fc64 (no description set)\nParent commit (@-): y c66bc991 (empty) refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<ul>\n<li><code>R</code> = Renamed. <code>jj</code> is pretty clever here. Since I moved the files but their\ncontents stayed the same, <code>jj</code> detected that I didn’t just “delete” one file\nand “add” a new one, I actually moved an object from point A to point B.\n<ul>\n<li>The fact that <code>jj</code> shows <code>R {home.nix =&gt; hosts/magic/home.nix}</code> means it is\nkeeping the history of that file intact. If you were to look at the log for\n<code>hosts/magic/home.nix</code> later, <code>jj</code> would know to look back into the history\nof the old <code>home.nix</code> as well.</li>\n</ul>\n</li>\n</ul>\n<p>I’m happy with the changes so far:</p>\n<pre><code class=\"language-bash\">❯  jj squash\nWorking copy  (@) now at: k 41deb985 (empty) (no description set)\nParent commit (@-)      : y 2bee669a refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<ul>\n<li>Notice how change <code>w</code> disappeared and <code>y</code> is no longer empty? That’s because\nwe squashed the changes from our Working copy into the parent commit!</li>\n</ul>\n<h3>Pushing from the squash workflow</h3>\n<p>Let’s look at what we have:</p>\n<pre><code class=\"language-bash\">❯  jj st\nThe working copy has no changes.\nWorking copy  (@) : k 41deb985 (empty) (no description set)\nParent commit (@-): y 2bee669a refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<p>Since the working copy is at an <code>(empty)</code> change, it wouldn’t make sense to push\nit. We have to move our bookmark to the parent commit, and then push!</p>\n<pre><code class=\"language-bash\">❯  jj bookmark set main -r @-\nMoved 1 bookmarks to y 2bee669a main* | refactor: restructure flake to multi-host layout in hosts/magic\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj git push\nChanges to push to origin:\n  Move forward bookmark main from e57a7a39957a to 2bee669ac276\ngit: Enumerating objects: 5, done.\ngit: Counting objects: 100% (5/5), done.\ngit: Delta compression using up to 16 threads\ngit: Compressing objects: 100% (3/3), done.\ngit: Writing objects: 100% (4/4), 452 bytes | 452.00 KiB/s, done.\ngit: Total 4 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)\nremote: Resolving deltas: 100% (1/1), completed with 1 local object.\n</code></pre>\n<p>This was the biggest Aha moment I had. It makes perfect sense that you wouldn’t\nwant to push a change that changes nothing. Since we squashed the contents of\nthe Working copy into <code>@-</code>, that is where we need <code>main</code> to point.</p>\n<p>I have an alias:</p>\n<pre><code class=\"language-nix\">  la = [\n    \"log\"\n    \"-r\"\n    \"all()\"\n  ];\n</code></pre>\n<p>You can also list all commits with:</p>\n<pre><code class=\"language-bash\">jj log -r ::\n# or\njj log r 'all()'\n</code></pre>\n<p>Let’s check out our full history so far:</p>\n<pre><code class=\"language-bash\">❯  jj la\n@  k sayls8@proton.me 2026-03-15 14:30:43 41deb985\n│  (empty) (no description set)\n◆  y sayls8@proton.me 2026-03-15 14:30:43 main 2bee669a\n│  refactor: restructure flake to multi-host layout in hosts/magic\n◆  u sayls8@proton.me 2026-03-15 14:07:28 e57a7a39\n│  feat: add minimal home.nix\n◆  m sayls8@proton.me 2026-03-15 13:43:31 b3cc09db\n│  feat: create minimal configuration.nix\n◆  p sayls8@proton.me 2026-03-15 13:41:19 cddde3b4\n│  chore: change username &amp; hostname in flake.nix\n◆  l sayls8@proton.me 2026-03-15 13:37:29 743b170d\n│  chore: Initialize system flake\n◆  z root() 00000000\n</code></pre>\n<ul>\n<li>\n<p>Textbook linear history. Every single commit has exactly one parent, forming a\nsingle, unbroken chain from the <code>root()</code> up to the current working copy.</p>\n</li>\n<li>\n<p>The diamonds <code>◆</code> show that everything from <code>y</code> down is now part of the\npermanent record (pushed to the remote).</p>\n</li>\n</ul>\n<p>In Git, achieving this usually requires <code>git add</code>, <code>git commit --amend</code>, or an\ninteractive rebase. In jj, you just worked in the working copy and pushed, the\ntool handled the “shaping” of the history for you.</p>\n<hr />\n<h2>Bookmarks and Branches</h2>\n<p>Bookmarks are named pointers to revisions (just like branches are in Git). You\ncan move them without affecting the target revision’s identity. – Jujutsu docs</p>\n<p>Branches are just multiple “changes” with the same parent.</p>\n<p>List all available bookmarks</p>\n<pre><code class=\"language-bash\">❯  jj bookmark list --all\nfeat1 (deleted)\n  @origin: xo 9067e7b7 mangowc flake-parts module\nmain: s b250e6ed testing the push-on-new non working bs\n  @git: s b250e6ed testing the push-on-new non working bs\n  @origin (behind by 2 commits): o 65e79e4b jj bookmarks push-on-new\nHint: Bookmarks marked as deleted can be *deleted permanently* on the remote by running `jj git push --deleted`. Use `jj bookmark forget` if you don't want that.\n</code></pre>\n<p>Every time you run <code>jj git push</code>, <code>jj</code> automatically runs <code>jj new main</code> for you\nbecause the working copy becomes immutable after the push.</p>\n<p>Show heads of all anonymous branches:</p>\n<pre><code class=\"language-bash\">jj log -r 'heads(all())'\n</code></pre>\n<p>To visualize an anonymous branch, Steve’s Jujutsu tutorial does a great job of\ndisplaying this:</p>\n<pre><code class=\"language-text\">\n                     ┌───┐ ┌───┐\n                 ┌───┤ F ◄─┤ G │\n                 │   └───┘ └───┘\n                 │\n ┌───┐  ┌───┐  ┌─▼─┐ ┌───┐ ┌───┐\n │ A ◄──┤ B ◄──┤ C ◄─┤ D ◄─┤ E │\n └───┘  └───┘  └───┘ └───┘ └───┘\n\n</code></pre>\n<p>Here, we’d say that <code>F</code> and <code>G</code> are two changes that are “on a branch,” because\nit looks like they’re branching off from <code>D</code> and <code>E</code>.\n<a href=\"https://steveklabnik.github.io/jujutsu-tutorial/branching-merging-and-conflicts/anonymous-branches.html#what-is-a-branch-conceptually\">Steves Jujutsu tutorial What is a branch conceptually?</a></p>\n<p>The above sentence confused me a bit. <code>F</code> and <code>G</code> are described as branching\n“off” of the line containing <code>D</code> and <code>E</code>. However, in the literal graph\nstructure, they diverge from <code>C</code>.</p>\n<p>Since <code>F</code> and <code>D</code> both point to <code>C</code>, they are “siblings”. Because no other\ncommits point back to <code>E</code> and <code>G</code>, they are the only two heads.</p>\n<p>In Jujutsu, a branch is just any path of commits that hasn’t been merged yet.\nSince E and G are both visible and unmerged, we have two “anonymous branches”\ncurrently active.</p>\n<p>The output of <code>jj log -r 'heads(all())'</code> with the above example, would yield:</p>\n<pre><code class=\"language-text\">○  G\n│\n~\n│\n○  E\n│\n~\n</code></pre>\n<ul>\n<li><code>G</code> and <code>E</code> are the heads of the anonymous branches.</li>\n</ul>\n<h2>Examples</h2>\n<p>Start at an empty change:</p>\n<pre><code class=\"language-bash\">❯  jj log\n@  p saylesss87@proton.me 2026-03-24 18:05:54 6469af06\n│  (no description set)\n</code></pre>\n<p>Give the current change a description:</p>\n<pre><code class=\"language-bash\">jj desc -m \"refactor(waybar): waybar flake-parts module\"\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj log\n@  p saylesss87@proton.me 2026-03-24 18:06:31 627d846c\n│  refactor(waybar): waybar flake-parts module\n○  yl saylesss87@proton.me 2026-03-24 17:55:00 56d13161\n│  refactor(foot): foot flake-parts module\n</code></pre>\n<p>To create a branch we need 2 changes with the same parent, our current changes\nparent is <code>yl</code> so let’s make our new change off of that:</p>\n<pre><code class=\"language-bash\">❯  jj new yl -m \"chore: add better documentation to README\"\nWorking copy  (@) now at: v 4697897a (empty) chore: add better documentation to README\nParent commit (@-)      : yl 56d13161 refactor(foot): foot flake-parts module\nAdded 1 files, modified 1 files, removed 1 files\n</code></pre>\n<p>Let’s check out our log to see our anonymous branches:</p>\n<pre><code class=\"language-bash\">❯  jj\n@  v saylesss87@proton.me 2026-03-24 18:12:04 4697897a\n│  (empty) chore: add better documentation to README\n│ ○  p saylesss87@proton.me 2026-03-24 18:06:31 627d846c\n├─╯  refactor(waybar): waybar flake-parts module\n○  yl saylesss87@proton.me 2026-03-24 17:55:00 56d13161\n│  refactor(foot): foot flake-parts module\n</code></pre>\n<p>We can see that <code>p</code> branches off from <code>yl</code>, let’s make this change before\nswitching to the other branch.</p>\n<pre><code class=\"language-bash\">❯  jj st\nWorking copy changes:\nM README.md\nWorking copy  (@) : v 23d94e56 chore: add better documentation to README\nParent commit (@-): yl 56d13161 refactor(foot): foot flake-parts module\n</code></pre>\n<pre><code class=\"language-bash\"># This effectively moves the working copy to the other \"branch\"\n❯  jj edit p\nWorking copy  (@) now at: p 627d846c refactor(waybar): waybar flake-parts module\nParent commit (@-)      : yl 56d13161 refactor(foot): foot flake-parts module\nAdded 1 files, modified 2 files, removed 1 files\n</code></pre>\n<blockquote>\n<p>In Git, you <code>checkout</code> a branch name; in <code>jj</code>, you just move your working copy\n(<code>@</code>) to whichever commit you want to build on.</p>\n</blockquote>\n<p>I’ve added the new flake-parts module and deleted the old home-manager style\nmodule:</p>\n<pre><code class=\"language-bash\">❯  jj st\nWorking copy changes:\nD home/waybar.nix\nM hosts/magic/home.nix\nA parts/waybar.nix\nWorking copy  (@) : p 627d846c refactor(waybar): waybar flake-parts module\nParent commit (@-): yl 56d13161 refactor(foot): foot flake-parts module\n</code></pre>\n<p>Let’s add another change on this branch to solidify these concepts:</p>\n<pre><code class=\"language-bash\">❯  jj new -m \"refactor(nh): nh flake-parts module\"\nWorking copy  (@) now at: n ac086b55 (empty) refactor(nh): nh flake-parts module\nParent commit (@-)      : p 627d846c refactor(waybar): waybar flake-parts module\n</code></pre>\n<p>And our log to show that this branch has grown:</p>\n<pre><code class=\"language-bash\">❯  jj log\n@  n saylesss87@proton.me 2026-03-24 18:22:12 ac086b55\n│  (empty) refactor(nh): nh flake-parts module\n○  p saylesss87@proton.me 2026-03-24 18:06:31 627d846c\n│  refactor(waybar): waybar flake-parts module\n│ ○  v saylesss87@proton.me 2026-03-24 18:17:51 23d94e56\n├─╯  chore: add better documentation to README\n○  yl saylesss87@proton.me 2026-03-24 17:55:00 56d13161\n│  refactor(foot): foot flake-parts module\n</code></pre>\n<p>Now, let’s first merge the README branch into this one. We are worried about the\nheads of the anonymous branches right now:</p>\n<pre><code class=\"language-bash\">❯  jj log -r 'heads(all())'\n@  n saylesss87@proton.me 2026-03-24 18:22:12 ac086b55\n│  (empty) refactor(nh): nh flake-parts module\n~\n\n○  v saylesss87@proton.me 2026-03-24 18:17:51 23d94e56\n│  chore: add better documentation to README\n</code></pre>\n<p>A merge is a new change that has more than one parent. With JJ, you make a\nchange with <code>jj new</code>. We can see that we need to make a change with both <code>n</code> and\n<code>v</code> as parents from the log output above:</p>\n<pre><code class=\"language-bash\">❯  jj new n v -m \"feat: merge in README docs\"\nWorking copy  (@) now at: yn a923db65 (empty) feat: merge in README docs\nParent commit (@-)      : n ac086b55 (empty) refactor(nh): nh flake-parts module\nParent commit (@-)      : v 23d94e56 chore: add better documentation to README\nAdded 0 files, modified 1 files, removed 0 files\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj log\n@    yn saylesss87@proton.me 2026-03-24 18:28:47 a923db65\n├─╮  (empty) feat: merge in README docs\n│ ○  v saylesss87@proton.me 2026-03-24 18:17:51 23d94e56\n│ │  chore: add better documentation to README\n○ │  n saylesss87@proton.me 2026-03-24 18:22:12 ac086b55\n│ │  (empty) refactor(nh): nh flake-parts module\n○ │  p saylesss87@proton.me 2026-03-24 18:06:31 627d846c\n├─╯  refactor(waybar): waybar flake-parts module\n○  yl saylesss87@proton.me 2026-03-24 17:55:00 56d13161\n│  refactor(foot): foot flake-parts module\n</code></pre>\n<p>That messes up our perfectly linear history, let’s rebase instead.</p>\n<pre><code class=\"language-bash\">❯  jj undo\nUndid operation: a74a45f02043 (2026-03-24 18:28:47) new empty commit\nRestored to operation: 848c180f2a04 (2026-03-24 18:22:12) new empty commit\nWorking copy  (@) now at: n ac086b55 (empty) refactor(nh): nh flake-parts module\nParent commit (@-)      : p 627d846c refactor(waybar): waybar flake-parts module\nAdded 0 files, modified 1 files, removed 0 files\n</code></pre>\n<p>Rebase is your tool for changing the ‘parent’ of a commit. If you have two\nparallel features (siblings) and you decide one should follow the other (linear\nstack), you rebase the second feature onto the first.</p>\n<pre><code class=\"language-bash\">❯  jj rebase -r v -o n\nRebased 1 commits to destination\n</code></pre>\n<p>Now our history is back to being completely linear:</p>\n<pre><code class=\"language-bash\">❯  jj log\n○  v saylesss87@proton.me 2026-03-24 18:30:36 8101b0dd\n│  chore: add better documentation to README\n@  n saylesss87@proton.me 2026-03-24 18:22:12 ac086b55\n│  (empty) refactor(nh): nh flake-parts module\n○  p saylesss87@proton.me 2026-03-24 18:06:31 627d846c\n│  refactor(waybar): waybar flake-parts module\n○  yl saylesss87@proton.me 2026-03-24 17:55:00 56d13161\n│  refactor(foot): foot flake-parts module\n</code></pre>\n<p>To move a stack of changes, use <code>jj rebase -s [Source] -d [Destination]</code>. If you\nwant to move a feature to the very tip of your main line, destination is <code>main</code>.\nIf you want to chain features together, destination is the previous feature’s\nhead.</p>\n<p>Pay attention to where <code>@</code> is while rebasing, to continue on this linear stack\nI’ll have to run <code>jj new v</code>.</p>\n<hr />\n<h2>Collaborating and opening PRs</h2>\n<p>So far all the examples assumed you are the only person touching this repo. For\ncollaboration (GitHub / GitLab PRs, code review, etc.), the mental model is:</p>\n<ul>\n<li>\n<p>GitHub only understands <em>branches</em> and <em>commits</em>.</p>\n</li>\n<li>\n<p>Jujutsu gives you <em>changes</em> and <em>bookmarks</em>.</p>\n</li>\n<li>\n<p>You use <code>jj git push</code> to translate your clean JJ history into a Git branch\nthat others can review.</p>\n</li>\n</ul>\n<p>A simple pattern that works well for feature branches and PRs:</p>\n<ol>\n<li>Start from <code>main</code></li>\n</ol>\n<p>Make sure main is up to date and your working copy is clean:</p>\n<pre><code class=\"language-bash\">jj git fetch\njj edit main\njj st\n</code></pre>\n<p>You should see an <code>(empty)</code> working copy with <code>main</code> as the parent.</p>\n<ol start=\"2\">\n<li>Create a named feature branch as a bookmark</li>\n</ol>\n<p>In JJ, you don’t “checkout” a branch, you create a new change and (optionally)\ngive it a bookmark name:</p>\n<pre><code class=\"language-bash\"># Create a new change on top of main and start working there\njj new main -m \"feat: add magic host\"\n\n# Optionally create a bookmark that GitHub will see as a branch\njj bookmark create feature/magic-host -r @\n</code></pre>\n<ul>\n<li>\n<p><code>jj new main</code> makes a sibling of any in‑progress work and starts a fresh\nchange on top of <code>main</code>.</p>\n</li>\n<li>\n<p>The bookmark <code>feature/magic-host</code> is what will become the Git branch name when\nyou push.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Hack, split, squash as usual</li>\n</ol>\n<p>Work in your normal JJ style:</p>\n<pre><code class=\"language-bash\"># edit files\njj st\njj split -i\njj desc -m \"feat: add magic host\"\njj new\n# more changes, more desc/split/squash, etc.\n</code></pre>\n<p>All of this is still local, fully mutable history.</p>\n<ol start=\"4\">\n<li>Point your feature bookmark at the top of the stack</li>\n</ol>\n<p>When you’re happy with the stack you want to send for review, move the bookmark\nto the tip:</p>\n<pre><code class=\"language-bash\"># If your working copy @ is the commit you want reviewed:\njj bookmark set feature/magic-host -r @\n\n# If you used the squash workflow and @ is empty:\njj bookmark set feature/magic-host -r @-\n</code></pre>\n<p>Rule of thumb:</p>\n<ul>\n<li>\n<p>If <code>jj st</code> shows actual changes or a non‑empty description at <code>@</code>, use <code>@</code>.</p>\n</li>\n<li>\n<p>If <code>@</code> is (empty) because you squashed into the parent, use <code>@-</code>.</p>\n</li>\n</ul>\n<ol start=\"5\">\n<li>Push to Git and open the PR</li>\n</ol>\n<p>Now push just like before, but your feature bookmark will become a branch on the\nremote:</p>\n<pre><code class=\"language-bash\">jj git push\n</code></pre>\n<p>This will:</p>\n<ul>\n<li>\n<p>Update <code>origin/feature/magic-host</code> to point at the same commit as your local\n<code>feature/magic-host</code>.</p>\n</li>\n<li>\n<p>Leave <code>main</code> alone until you explicitly move and push it.</p>\n</li>\n</ul>\n<p>On GitHub/GitLab:</p>\n<ul>\n<li>\n<p>You’ll see a branch named <code>feature/magic-host</code>.</p>\n</li>\n<li>\n<p>Open a PR from <code>feature/magic-host</code> into <code>main</code> as usual.</p>\n</li>\n</ul>\n<ol start=\"6\">\n<li>Iterate on review feedback</li>\n</ol>\n<p>If a reviewer asks for changes:</p>\n<ol>\n<li>Come back to your feature branch stack:</li>\n</ol>\n<pre><code class=\"language-bash\">jj edit feature/magic-host\n</code></pre>\n<ol start=\"2\">\n<li>\n<p>Make your edits, split/squash/reword history freely.</p>\n</li>\n<li>\n<p>Move the bookmark to the new tip and push again:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">jj bookmark set feature/magic-host -r @\njj git push\n</code></pre>\n<p>GitHub will show the PR updating in place, but you got to reshuffle history\nlocally without <code>git rebase -i</code> pain.</p>\n<p>“I just want a one‑off PR, no named bookmark”</p>\n<p>If you don’t care about a persistent bookmark and just want a quick “one‑shot”\nPR from whatever you’re currently working on:</p>\n<ol>\n<li>\n<p>Make sure your stack is in the shape you want.</p>\n</li>\n<li>\n<p>Put main on the top of that stack:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\"># Working copy @ is the tip you want:\njj bookmark set main -r @\n# Or equivalently `jj bookmark set main` since commands default to the working copy\njj git push\n</code></pre>\n<ol start=\"3\">\n<li>On GitHub, create the PR from your fork’s main to upstream main.</li>\n</ol>\n<p>This is essentially the same pattern we already use in the “squash workflow,”\nbut applied with the mental model: “move the bookmark people care about (<code>main</code>\nor a feature name) to the commit I want them to review, then push.”</p>\n<h3>Workflow Considerations</h3>\n<p>You may want to change up how you work depending on your requirements…</p>\n<ol>\n<li>The Tower Workflow (The “Dependent Stack”)</li>\n</ol>\n<p>You use a tower when your changes build on top of each other.</p>\n<ul>\n<li>\n<p><code>main</code> → <code>Feature A</code> → <code>Feature B</code> → <code>Feature C</code></p>\n</li>\n<li>\n<p>Use it when you are refactoring a Nix module (Feature A), and then you need to\nuse that new module to configure your desktop (Feature B). You literally\ncannot do B without A.</p>\n</li>\n</ul>\n<p>The Collaborative Benefit: You can push the entire stack. Your teammates see the\nlogical progression of your thought process. They can review “Feature A” while\nyou are already working on “Feature C.”</p>\n<ol start=\"2\">\n<li>The Sibling Workflow (<code>jj new main</code>) You use <code>jj new main</code> when you are\nworking on independent ideas that have nothing to do with each other.</li>\n</ol>\n<ul>\n<li>How it looks: * <code>main</code> → <code>Fix-Helix-Keys</code>\n<ul>\n<li>\n<p><code>main</code> → <code>Update-Nix-Channel</code></p>\n</li>\n<li>\n<p><code>main</code> → <code>New-Wallpaper-Script</code></p>\n</li>\n</ul>\n</li>\n</ul>\n<p>When to use it: You’re in the middle of a massive system refactor, but you\nsuddenly notice your Helix C-p bind is broken. You don’t want the Helix fix to\nbe “trapped” behind the refactor.</p>\n<p>The Collaborative Benefit: Isolation. If your “System Refactor” is buggy and\ntakes three days to fix, you can still <code>jj git push</code> the “Helix Fix” to <code>main</code>\nimmediately because it’s a direct child of <code>main</code>. It isn’t “waiting” for the\nother commits.</p>\n<p><strong>Fixing Divergent Branches</strong></p>\n<p><code>jj</code> makes it easy to fix divergent branches with <code>jj forget</code>.</p>\n<p>As an example imagine this scenario:</p>\n<ol>\n<li>\n<p>On your Laptop: You finish a feature and jj git push. Local main and\norigin/main are now at Revision B.</p>\n</li>\n<li>\n<p>On your Desktop: You forgot to pull. Local main is still at Revision A. You\nstart hacking and create Revision C.</p>\n</li>\n<li>\n<p>The Mess: You run <code>jj git fetch</code>. Now your Desktop sees:</p>\n</li>\n</ol>\n<ul>\n<li>\n<p><code>main</code> (local) at C</p>\n</li>\n<li>\n<p><code>main@origin</code> at B</p>\n</li>\n<li>\n<p><code>jj</code> freaks out and marks the bookmark as diverged (main*).</p>\n</li>\n</ul>\n<p><code>jj bookmark forget main</code> is the “I don’t want to think about it” button. It\ndeletes the <code>main*</code> label on your Desktop so you can just fetch the “real” one\nfrom the Laptop/GitHub and rebase your new work (C) on top of it.</p>\n<hr />\n<h2>Tips &amp; Tricks</h2>\n<p>Your History is just a stack of diffs:</p>\n<ul>\n<li>\n<p><code>jj commit -i</code>: Slices a diff off your current work.</p>\n</li>\n<li>\n<p><code>jj split -i</code>: Slices an existing diff into two.</p>\n</li>\n<li>\n<p><code>jj bookmark forget</code>: Deletes a label that got messy.</p>\n</li>\n</ul>\n<p><strong>The Ghost Refactor</strong></p>\n<p><strong>The Scenario</strong>: You’re in the middle of a complex Rust feature (Change C), and\nyou realize a function in the base (Change A) needs to be public or renamed for\nthis to work.</p>\n<ol>\n<li>\n<p><code>jj new A -m \"quick fix\"</code> → Creates a new “slice” right after A.</p>\n</li>\n<li>\n<p>Make your change.</p>\n</li>\n<li>\n<p><code>jj squash</code> → This “melts” the fix directly into A.</p>\n</li>\n</ol>\n<hr />\n<p><strong>Use <code>jj describe</code> as a Task List</strong></p>\n<p>Because <code>jj</code> doesn’t require a “commit” to save work, you can use descriptions\nto manage your focus.</p>\n<p>When you start a session, create a few empty changes:</p>\n<ol>\n<li>\n<p><code>jj new main -m \"Update Cargo.toml\"</code></p>\n</li>\n<li>\n<p><code>jj new -m \"Add pixel conversion logic\"</code></p>\n</li>\n<li>\n<p><code>jj new -m \"Fix CLI output formatting\"</code></p>\n</li>\n</ol>\n<p>Now we have a roadmap. Use <code>jj edit</code> to jump into whichever task you feel like\ndoing. <code>jj</code> tracks the progress of each. No need to <code>stash</code> when jumping between\ntasks because of the working copy commit.</p>\n<hr />\n<p><strong>Breaking up your Current set of Changes into Atomic Commits: “The Atomic\nShredder” <code>jj split</code></strong></p>\n<p>Often I’ll just keep working on a bug or feature until it works, not\nparticularly concerned about my VCS history. Let’s say I spent 3 hours hacking\non a project of mine to get a feature working and I touched 10 files. It works,\nbut the commit is a mess.</p>\n<p>With the <code>gitpatch</code> tool, <code>jj split -i</code> makes it simple to break down many\nchanges into logical “atomic commits” using a simple <code>y</code>/ <code>n</code> interface.</p>\n<p>You can actually use both <code>jj commit -i</code> &amp; <code>jj split -i</code> to break down changes\nin <code>@</code> into smaller changes. <code>jj commit -i</code> is ergonomically tuned for “push\nsome current work down, leave the rest in @”.</p>\n<p>If your changes are already in <code>@-</code> (or any earlier commit):</p>\n<ul>\n<li>\n<p><code>jj commit -i</code> can’t help, because it only ever operates on <code>@</code>.</p>\n</li>\n<li>\n<p><code>jj split -i -r @-</code> (or equivalent) can be used to modify an existing commit\nin history.</p>\n</li>\n</ul>\n<p><strong>As a rule of thumb</strong>:</p>\n<ul>\n<li>\n<p>Changes in <code>@</code> → use <code>jj commit -i</code> to peel off a clean piece, and also\n<code>jj squash -i</code> to squash a subset of changes from the working copy into the\nparent commit.</p>\n</li>\n<li>\n<p>Changes in <code>@-</code> (or earlier) → use <code>jj split -i</code> to rewrite that commit</p>\n</li>\n</ul>\n",
      "date_published": "2026-03-16T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/dendritic_flake_parts.html",
      "url": "https://saylesss88.github.io/flakes/dendritic_flake_parts.html",
      "title": "The Dendritic Pattern with flake-parts",
      "content_html": "<h1>The Dendritic Pattern with flake-parts</h1>\n<p><img src=\"https://saylesss88.github.io/../images/dendritic_nix.png\" alt=\"Dendritic Logo\" />–<a href=\"https://github.com/mightyiam/dendritic\">dendretic repo</a></p>\n<p>In the early days of Flakes, users often ended up with a massive, monolithic\n<code>flake.nix</code> or a spaghetti-like web of manual imports. The Dendritic (Tree-like)\nPattern solves this by treating your filesystem as the source of truth, using\n<code>flake-parts</code> as the nervous system that routes code to the correct outputs.</p>\n<h2>Flake-Parts</h2>\n<p>IMO the <code>flake-parts</code> docs could do a lot better at explaining how to use it to\nconfigure your system. I’ll attempt to explain how to use it and why you might\nwant to.</p>\n<p>While <code>flake-parts</code> provides the core structure for standard flake outputs, its\nreal power lies in its modular ecosystem. You can plug in opinionated modules to\ninstantly add specialized features to your system’s nervous system.</p>\n<ul>\n<li>When people say “top-level flake attribute”, they mean putting your\nconfiguration inside the <code>flake = { ... };</code> block.</li>\n</ul>\n<h3>The “Top-Level” (<code>flake</code>):</h3>\n<p>This is for things that are <strong>global</strong> and don’t change regardless of whether\nyou’re on a Mac, PC or ARM server.</p>\n<ul>\n<li>\n<p>Example: Your <code>nixosConfigurations</code> or <code>homeConfigurations</code>.</p>\n</li>\n<li>\n<p>A NixOS configuration for a specific laptop is a single, static definition. It\ndoesn’t need to be “multiplied” by system types.</p>\n</li>\n</ul>\n<h2>The “Per-System” (<code>perSystem</code>)</h2>\n<ul>\n<li>\n<p><code>perSystem</code> is for things that must be built for a specific architecture.\n(e.g., <code>devShells</code>, <code>packages</code>, <code>formatter</code>)</p>\n<ul>\n<li>You can’t run an <code>x86_64</code> version of <code>helix</code> on an <code>aarch64</code> (ARM) MacBook.</li>\n</ul>\n</li>\n<li>\n<p>Our <code>nixosConfigurations</code> don’t live in a system specific attribute so it goes\nunder <code>flake</code> instead of <code>perSystem</code>.</p>\n</li>\n</ul>\n<p>You can place everything in the same file:</p>\n<pre><code class=\"language-nix\">outputs = inputs@{ flake-parts, ... }:\n  # https://flake.parts/module-arguments.html\n  flake-parts.lib.mkFlake { inherit inputs; } (top@{ config, withSystem, moduleWithSystem, ... }: {\n    imports = [\n      # Optional: use external flake logic, e.g.\n      # inputs.foo.flakeModules.default\n    ];\n    flake = {\n      # Put your original flake attributes here.\n    };\n    systems = [\n      # systems for which you want to build the `perSystem` attributes\n      \"x86_64-linux\"\n      # ...\n    ];\n    perSystem = { config, pkgs, ... }: {\n      # Recommended: move all package definitions here.\n      # e.g. (assuming you have a nixpkgs input)\n      # packages.foo = pkgs.callPackage ./foo/package.nix { };\n      # packages.bar = pkgs.callPackage ./bar/package.nix {\n      #   foo = config.packages.foo;\n      # };\n    };\n  });\n</code></pre>\n<p>Or you can break it down into modules:</p>\n<pre><code class=\"language-nix\">{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    flake-parts.url = \"github:hercules-ci/flake-parts\";\n  };\n\n  outputs = inputs @ { flake-parts, ... }:\n    flake-parts.lib.mkFlake { inherit inputs; }\n    {\n      # Supported Systems\n      systems = [ \"x86_64-linux\" \"x86_64-darwin\"];\n      imports = [ ./devShell.nix ./package.nix ./nixos.nix ]\n    };\n\n}\n</code></pre>\n<p><code>devShell.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  perSystem = { pkgs, ...}: {\n  devShells.default = pkgs.mkShell {\n    packages = [ pkgs.ripgrep pkgs.fd ];\n  };\n};\n}\n</code></pre>\n<p><code>package.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  perSystem = { pkgs, ...}: {\n    packages.myPackage = pkgs.myPackage;\n  };\n}\n</code></pre>\n<h2>Getting Started</h2>\n<p>This is the flake that I’m currently using to slowly adopt the dendritic\npattern, or not if I choose not to. Maybe making every single thing a flake\noutput isn’t necessary or the best idea, we’ll see. With this, you can use both\nregular NixOS/home-manager modules and NixOS/home-manager flake-parts modules.</p>\n<p><strong>Scalability Toggle</strong>:</p>\n<ul>\n<li>\n<p>I have <code>++ (builtins.attrValues ...)</code> set for both home-manager and NixOS\nwhere it automatically adds the inputs for files in the <code>~/flake/parts</code>\ndirectory. This is fine for personal use where you want the custom modules\navailable everywhere instantly.</p>\n</li>\n<li>\n<p>If the flake gets too big and you want more control just comment out those 2\nlines in the <code>nixos.nix</code> and start explicitly adding the <code>inputs</code> to your\n<code>imports</code> list. Now the <code>parts/</code> directory will act like a library rather than\na Registry, you manually pick what you want for each host.</p>\n</li>\n<li>\n<p><a href=\"https://nix-community.github.io/home-manager/index.xhtml#sec-flakes-flake-parts-module\">home-manager flake-parts module</a></p>\n</li>\n</ul>\n<p>Example: Let’s call this <code>~/flake/flake.nix</code></p>\n<pre><code class=\"language-nix\"># In this example the top-level configuration is a [`flake-parts`](https://flake.parts) one.\n# Therefore, every Nix file (other than this) is a flake-parts module.\n# https://github.com/mightyiam/dendritic/blob/master/example/flake.nix\n{\n  # Declares flake inputs\n  inputs = {\n    flake-parts = {\n      url = \"github:hercules-ci/flake-parts\";\n      inputs.nixpkgs-lib.follows = \"nixpkgs\";\n    };\n\n    import-tree.url = \"github:vic/import-tree\";\n\n    nixpkgs.url = \"github:nixos/nixpkgs/25.11\";\n  };\n\n  outputs =\n    inputs:\n    inputs.flake-parts.lib.mkFlake { inherit inputs; } {\n      # This tells flake-parts which systems to generate outputs for\n      systems = import inputs.systems;\n\n      imports = [\n        # Optional: use external flake logic, e.g.\n        inputs.treefmt-nix.flakeModule\n        # Import home-manager's flake module\n        # The flake module defines flake.homeModules and flake.homeConfigurations options,\n        # allowing them to be properly merged if they are defined in multiple modules\n        inputs.home-manager.flakeModules.default\n        ./nixos.nix\n      ]\n      ++ (inputs.import-tree ./parts).imports;\n\n      hosts = {\n        magic = {\n          username = \"jr\";\n          system = \"x86_64-linux\";\n        };\n        # Adding a second machine is now 4 lines of code:\n        # secondary = { username = \"jr\"; system = \"aarch64-linux\"; };\n      };\n\n      perSystem =\n        {\n          system,\n          ...\n        }:\n        {\n\n          # Access pkgs with your specific config\n          _module.args.pkgs = import inputs.nixpkgs {\n            inherit system;\n            config.allowUnfree = false;\n          };\n        };\n\n    };\n}\n</code></pre>\n<blockquote>\n<p><code>import-tree</code> is essentially a “smarter” version of the <code>scanPaths</code> function\nthat we’ll see later specifically designed for <code>flake-parts</code>. It ensures that\nevery file in <code>./parts</code> is treated as a module that the <code>mkFlake</code> engine can\ndigest.</p>\n</blockquote>\n<p>Note, this example has more code than for example the dendritic nix repo has\nbecause it enables you to automatically import <code>flake-parts</code> modules as well as\nstandard NixOS and home-manager modules as you move to the new system/pattern.</p>\n<p>And <code>~/flake/nixos.nix</code>:</p>\n<pre><code class=\"language-nix\">/**\n  System Configuration Factory Module\n\n  This module defines a custom schema for describing multiple NixOS hosts\n  and automatically generates the corresponding `nixosConfigurations` flakes output.\n*/\n{\n  inputs,\n  self,\n  lib,\n  config,\n  ...\n}:\nlet\n  # Internal Library &amp; Module Imports\n\n  # Initialize a custom library using the project's internal lib and nixpkgs\n  myLib = import \"${self}/lib/default.nix\" { inherit (inputs.nixpkgs) lib; };\n\n  # Import entry points for global NixOS and Home Manager shared modules\n  # `self` points to the root of the flake (requires passing `self` throuth specialArgs)\n  nixosModules = import \"${self}/nixos\";\n  homeManagerModules = import \"${self}/home\";\n\n  # Shared Binary Cache Configuration\n  caches = {\n    nix.settings = {\n      builders-use-substitutes = true;\n      substituters = [ \"https://cache.nixos.org\" ];\n      trusted-public-keys = [ \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\" ];\n    };\n  };\nin\n{\n  /**\n    SCHEMA DEFINITION\n    Defines the 'hosts' option, allowing us to declare system metadata\n    (e.g., username, architecture) in a structured attribute set.\n  */\n  options.hosts = lib.mkOption {\n    description = \"An attribute set of host definitions to be generated.\";\n    type = lib.types.attrsOf (\n      lib.types.submodule {\n        options = {\n          username = lib.mkOption {\n            type = lib.types.str;\n            default = \"jr\";\n            description = \"Primary user account name for this host.\";\n          };\n          system = lib.mkOption {\n            type = lib.types.str;\n            default = \"x86_64-linux\";\n            description = \"The target system architecture.\";\n          };\n        };\n      }\n    );\n  };\n  /**\n    CONFIGURATION GENERATION\n    Iterates through the 'config.hosts' defined above and maps them to\n    actual 'nixosSystem' instances for the Flake output.\n  */\n  config.flake.nixosConfigurations = lib.mapAttrs (\n    host: cfg:\n    inputs.nixpkgs.lib.nixosSystem {\n      # Pass global context and metadata into the module system\n      specialArgs = {\n        inherit\n          inputs\n          self\n          host\n          myLib\n          ;\n        inherit (cfg) username;\n      };\n\n      modules = [\n        # 1. Project-wide NixOS logic\n        nixosModules\n\n        # 2. Host-specific hardware/system configuration file\n        \"${self}/hosts/${host}/configuration.nix\"\n\n        # 3. Home Manager NixOS module (allows configuring HM within NixOS)\n        inputs.home-manager.nixosModules.home-manager\n\n        # 4. Standardized cache settings defined in 'let' block\n        caches\n\n        # 5. Inline configuration for system-specific and user-specific settings\n        {\n          nixpkgs.hostPlatform = cfg.system;\n          home-manager = {\n            # By default, Home Manager uses a private pkgs instance that is configured\n            #  via the home-manager.users.&lt;name&gt;.nixpkgs options. To instead use the\n            #  global pkgs that is configured via the system level nixpkgs options, set\n            useGlobalPkgs = true;\n            # Install packages to /etc/profiles rather than $HOME/.nix-profile\n            useUserPackages = true;\n            # Dynamically import the user's home configuration based on host/username\n            users.${cfg.username} = {\n              imports = [\n                (import \"${self}/hosts/${host}/home.nix\")\n              ]\n              # Automatically import `homeModules` in `./parts`\n              # Comment out if you want to be explicit and add\n              # e.g., inputs.self.homeModules.helix\n              ++ (builtins.attrValues config.flake.homeModules);\n            };\n\n            extraSpecialArgs = {\n              inherit\n                inputs\n                homeManagerModules\n                myLib\n                host\n                ;\n              inherit (cfg) username;\n            };\n          };\n        }\n      ]\n      # Automatically import `nixosModules` in `./parts`\n      ++ (builtins.attrValues config.flake.nixosModules);\n    }\n  ) config.hosts;\n}\n</code></pre>\n<p>Now, any <code>flake-parts</code> module that we place in the <code>~/flake/parts/</code> directory\nwill be automatically imported with <code>import-tree</code>. The key here is that it has\nto be a <code>flake-parts</code> module, i.e. wrapped in <code>flake</code> or <code>perSystem</code>, etc.</p>\n<p>You can place both NixOS modules and home-manager modules in the <code>~/flake/parts</code>\ndirectory. Just import it to the correct location and you’re good.</p>\n<p>Example NixOS module for amd drivers <code>~/flake/parts/amd-drivers.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  flake.nixosModules.amd-drivers =\n    {\n      lib,\n      pkgs,\n      config,\n      ...\n    }:\n    with lib;\n    let\n      cfg = config.custom.amd-drivers;\n    in\n    {\n      options.custom.amd-drivers.enable = mkEnableOption \"AMD GPU/CPU optimized for AM06 Pro\";\n\n      config = mkIf cfg.enable {\n        # Modern ROCm/HIP support\n        systemd.tmpfiles.rules = [ \"L+ /opt/rocm/hip - - - - ${pkgs.rocmPackages.clr}\" ];\n        services.xserver.videoDrivers = [ \"amdgpu\" ];\n\n        hardware = {\n          amdgpu.initrd.enable = true;\n\n          graphics = {\n            enable = true;\n            enable32Bit = true;\n            extraPackages = with pkgs; [\n              rocmPackages.clr.icd # For OpenCL/Compute\n              # Hardware Acceleration (Video Encoding/Decoding)\n              libva\n              libva-utils\n              libva-vdpau-driver\n              libvdpau-va-gl\n            ];\n          };\n\n          cpu.amd.updateMicrocode = true;\n        };\n\n        boot = {\n          kernelModules = [\n            \"kvm-amd\"\n            \"amdgpu\"\n          ];\n          kernelParams = [\n            \"amd_pstate=active\" # Best for Ryzen 5000+ power management\n          ];\n        };\n\n        boot.kernelPackages = pkgs.linuxPackages_latest;\n      };\n    };\n}\n</code></pre>\n<p>Now, to enable this module, I’ll add this to my <code>configuration.nix</code> or\nequivalent:</p>\n<pre><code class=\"language-nix\"> {...}: {\n  imports = [\n        # Not necessary because of the `++ (builtins.attrValues config.flake.nixosModules)` in nixos.nix\n        # for more control remove those lines and explicitly add:\n        # inputs.self.nixosModules.amd-drivers\n  ];\n\n  custom = {\n    amd-drivers.enable = true;\n  };\n}\n# ---snip---\n</code></pre>\n<hr />\n<p>Example home-manager module <code>~/flake/parts/fzf.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  flake.homeModules.fzf =\n    { lib, config, ... }:\n    let\n      cfg = config.custom.fzf;\n    in\n    {\n      options.custom.fzf.enable = lib.mkEnableOption \"Enable fzf module\";\n\n      config = lib.mkIf cfg.enable {\n        programs.fzf = {\n          enable = true;\n          # colors = lib.mkForce { };\n\n          defaultOptions = [\n            \"--height 40%\"\n            \"--reverse\"\n            \"--border\"\n            \"--color=16\"\n          ];\n\n          defaultCommand = \"rg --files --hidden --glob=!.git/\";\n        };\n      };\n    };\n}\n</code></pre>\n<p>And enable with <code>custom.fzf.enable = true;</code> in your <code>home.nix</code> or equivalent.</p>\n<p>And this will be automatically imported, same as above but because of the added\n<code>++ (builtins.attrValues config.flake.homeModules);</code> in <code>nixos.nix</code>. Without\nthe automatic import, add <code>inputs.self.homeModules.fzf</code> to your <code>imports</code> list\nin your <code>home.nix</code> or equivalent.</p>\n<p>If you don’t like the auto-import behavior, just delete or comment out that\nline, after that, the <code>import</code> statements become necessary.</p>\n<hr />\n<details>\n<summary>Example of a module thats both NixOS and home-manager zsh.nix </summary>\n<p><code>~/flake/parts/shells/zsh.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  flake.nixosModules.zsh =\n    {\n      pkgs,\n      lib,\n      config,\n      username,\n      ...\n    }:\n    let\n      cfg = config.custom.zsh;\n    in\n    {\n      options.custom.zsh = {\n        enable = lib.mkEnableOption \"User zsh configuration\";\n      };\n\n      config = lib.mkIf cfg.enable {\n\n        # 1. NixOS System-Level (The \"Foundation\")\n        programs.zsh.enable = true;\n        users.defaultUserShell = pkgs.zsh;\n        environment.pathsToLink = [ \"/share/zsh\" ]; # Fixes completion for system packages\n\n        # 2. Home Manager (User Configuration)\n        home-manager.users.${username} = {\n          programs.zsh = {\n            enable = true;\n            enableCompletion = true;\n            completionInit = \"autoload -U compinit &amp;&amp; compinit\";\n            autosuggestion.enable = true;\n            syntaxHighlighting.enable = true;\n            oh-my-zsh = {\n              package = pkgs.oh-my-zsh;\n              enable = true;\n              plugins = [\n                \"git\"\n                \"sudo\"\n                \"rust\"\n                \"fzf\"\n              ];\n            };\n            profileExtra = ''\n              if [ -z \"$DISPLAY\" ] &amp;&amp; [ \"$XDG_VTNR\" = 1 ]; then\n               exec mango\n              fi\n              # ---snip----\n}\n</code></pre>\n<blockquote>\n<p>NOTE: Passing <code>username</code> through <code>specialArgs</code> is what makes this bridge work,\nyou can also just use your username.</p>\n</blockquote>\n<p>This is what <code>~/flake/parts/shells/default.nix</code> looks like, I had to explicitly\nadd <code>myLib</code> in a <code>let</code> statement to prevent infinite recursion:</p>\n<pre><code class=\"language-nix\">{ lib, ... }:\nlet\n  # prevents infinite recursion error\n  myLib = import ../../lib { inherit lib; };\nin\n{\n  # Now we can use it safely\n  imports = myLib.scanPaths ./.;\n}\n</code></pre>\n<p>Enable it in <code>configuration.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\">custom.zsh.enable = true;\n</code></pre>\n<p><code>lib/default.nix</code> is just a function that automatically imports any <code>.nix</code> file,\nskipping <code>default.nix</code>:</p>\n<details>\n<summary> `lib/default.nix` </summary>\n<pre><code class=\"language-nix\">{ lib, ... }:\n{\n  # Returns a list of all .nix files and directories in a path,\n  # skipping default.nix. Perfect for the 'imports' list.\n  scanPaths =\n    path:\n    let\n      content = builtins.readDir path;\n    in\n    map (name: path + \"/${name}\") (\n      builtins.attrNames (\n        lib.filterAttrs (\n          name: type: (type == \"directory\") || (name != \"default.nix\" &amp;&amp; lib.hasSuffix \".nix\" name)\n        ) content\n      )\n    );\n\n  relativeToRoot = lib.path.append ../.;\n}\n</code></pre>\n</details>\n<p>The key here is wrapping the home-manager logic in\n<code>home-manager.users.${username} = {}</code>, this effectively creates a home-manager\nsandbox enabling configuration of both in the same file.</p>\n<blockquote>\n<p>NOTE: You can do this without <code>flake-parts</code> also but often wasn’t recommended\nbecause the files become a mess that’s hard to understand.</p>\n</blockquote>\n</details>\n<hr />\n<h1>Example using perSystem</h1>\n<p>In the <code>flake.nix</code>, notice the <code>inputs.treefmt-nix.flakeModule</code>. Since a\nformatter is something that you would want to run on every system, you use the\n<code>perSystem</code> attribute.</p>\n<p>Adding the <code>inputs.treefmt-nix.flakeModule</code> makes the <code>treefmt</code> options\navailable</p>\n<p><code>~/flake/parts/treefmt.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  perSystem = _: {\n    treefmt = {\n      projectRootFile = \"flake.nix\";\n\n      programs = {\n        deadnix.enable = true;\n        statix.enable = true;\n        keep-sorted.enable = true;\n        nixfmt = {\n          enable = true;\n          # package = pkgs.nixfmt;\n        };\n      };\n\n      settings = {\n        global.excludes = [\n          \"LICENSE\"\n          \"README.md\"\n          \".adr-dir\"\n          \"nu_scripts\"\n          \"*.{gif,png,svg,tape,mts,lock,mod,sum,toml,env,envrc,gitignore,sql,conf,pem,key,pub,py,narHash}\"\n          \"Cargo.lock\"\n          \"flake.lock\"\n          \"justfile\"\n          \".jj/*\"\n        ];\n\n        formatter = {\n          nixfmt.priority = 1;\n          statix.priority = 2;\n          deadnix.priority = 3;\n        };\n      };\n    };\n  };\n}\n</code></pre>\n<blockquote>\n<p>By using <code>perSystem</code>, your <code>treefmt</code> configuration is automatically available\nfor every architecture you support (x86, ARM, etc.), allowing you to run\n<code>nix fmt</code> on any machine without rewriting the logic.</p>\n</blockquote>\n<p><code>import-tree</code> automatically imports this because it’s a <code>flake-parts</code> module &amp;\nflake output.</p>\n<hr />\n<h1>flake-parts &amp; numtide devshells</h1>\n<p>Adding this flake input and <code>flakeModule</code> make the options available, they’re\nsimilar to NixOS’s <code>devShell</code> but not the same:</p>\n<pre><code class=\"language-nix\">devshell.url = \"github:numtide/devshell\";\n</code></pre>\n<p><code>~/flake/parts/dev-shell.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  perSystem =\n    { pkgs, system, ... }:\n    {\n      devshells.default = {\n        name = \"nixos-dev\";\n\n        packages = with pkgs; [\n          nixfmt\n          deadnix\n          nixd\n          nil\n          nh\n          nix-diff\n          nix-tree\n          helix\n          git\n          ripgrep\n          jq\n          tree\n        ];\n\n        # Message of the Day\n        motd = ''\n          {2}── NixOS Dev Shell ──────────────────────────────────────────{reset}\n          {9}  System: {reset} ${system}\n          {2}──────────────────────────────────────────────────────────────{reset}\n        '';\n\n        commands = [\n          {\n            name = \"rebuild\";\n            package = \"nh\";\n            help = \"Run nh os switch on the current flake\";\n            command = \"nh os switch .\";\n          }\n          {\n            name = \"fmt\";\n            package = \"nixfmt\";\n            help = \"Format all nix files in the project\";\n            command = \"nix fmt\";\n          }\n        ];\n      };\n    };\n}\n</code></pre>\n<p>Enter devShell:</p>\n<pre><code class=\"language-bash\">cd ~/flake\nnix develop\n</code></pre>\n",
      "date_published": "2026-03-07T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/zfs_bare-metal.html",
      "url": "https://saylesss88.github.io/installation/enc/zfs_bare-metal.html",
      "title": "ZFS Bare Metal Impermanence",
      "content_html": "<h1>ZFS Imperm Bare-Metal</h1>\n<p>I couldn’t get disko to bend to my will so I wrote the following bash script.\nThe script automates the steps in Graham Christensen’s\n<a href=\"https://grahamc.com/blog/erase-your-darlings/\">Erase your darlings</a></p>\n<h2>The Storage Architecture</h2>\n<p>The script below automates the “Erase Your Darlings” setup. It organizes your\ndata into three distinct “levels” of persistence:</p>\n<ul>\n<li>\n<p><strong>The Volatile (<code>/</code>)</strong>: A ZFS dataset that is blank at boot. We take a\nsnapshot called @blank immediately after creation. In your NixOS\nconfiguration, you will set up a boot-time script to roll back to this @blank\nsnapshot, effectively “formatting” your root in milliseconds.</p>\n</li>\n<li>\n<p><strong>The Store (<code>/nix</code>)</strong>: A separate dataset for the Nix store. This doesn’t\nneed to be wiped because Nix already manages its own integrity.</p>\n</li>\n<li>\n<p><strong>The Safe (<code>/persist</code> and <code>/home</code>)</strong>: These datasets hold the things you\nactually care about—your SSH keys, browser profiles, and project files.</p>\n</li>\n</ul>\n<p><strong>What this script automates</strong></p>\n<p>This bash script handles the “Stage 0” heavy lifting. It will:</p>\n<ol>\n<li>\n<p><strong>Partition</strong> your disk with an EFI boot partition and a LUKS2 encrypted\ncontainer.</p>\n</li>\n<li>\n<p><strong>Initialize</strong> a ZFS pool (<code>rpool</code>) with performance-optimized settings (like\n<code>ashift=12</code> and <code>zstd</code> compression).</p>\n</li>\n<li>\n<p><strong>Carve</strong> out the datasets required for an Impermanence setup.</p>\n</li>\n<li>\n<p><strong>Mount</strong> the hierarchy into <code>/mnt</code> so <code>nixos-generate-config</code> can detect the\nspecialized ZFS layout.</p>\n</li>\n</ol>\n<blockquote>\n<p><strong>WARNING</strong>: This is a destructive operation. Running this script will wipe\nthe target drive completely. Ensure you have backed up any existing data\nbefore proceeding.</p>\n</blockquote>\n<ul>\n<li><a href=\"https://github.com/saylesss88/my-flake2/blob/main/install.sh\">The Setup Script</a></li>\n</ul>\n<h2>Quick Start</h2>\n<ol>\n<li>Start with the minimal ISO:</li>\n</ol>\n<ul>\n<li>\n<p><a href=\"https://nixos.org/download/\">NixOS Downloads</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/manual/nixos/stable/index.html#sec-installation-manual\">NixOS Manual Installation</a></p>\n</li>\n<li>\n<p>The script handles the partitioning, formatting, mounting, and lastly, runs\n<code>nixos-generate-config --root /mnt</code>. After running the script, edit the files\nin the repo matching your user and device. Finally, after you’re sure you\nhaven’t missed anything, run <code>nixos-install</code>.</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">export NIX_CONFIG='experimental-features = nix-command flakes'\n</code></pre>\n<ol start=\"2\">\n<li>Clone the <a href=\"https://github.com/saylesss88/my-flake2#\">starter repo</a>:</li>\n</ol>\n<pre><code class=\"language-bash\">git clone https://github.com/saylesss88/my-flake2.git\n</code></pre>\n<ol start=\"3\">\n<li>Inspect &amp; Run the script provided with the repo &amp; follow prompts.:</li>\n</ol>\n<blockquote>\n<p><strong>WARNING</strong>: This is a destructive operation. Running this script will wipe\nthe target drive completely. Ensure you have backed up any existing data\nbefore proceeding.</p>\n</blockquote>\n<pre><code class=\"language-bash\">sudo chmod +x ./install.sh\nsudo bash ./install.sh\n</code></pre>\n<ul>\n<li>I tested the script on an <code>nvme0n1</code> drive with no issues.</li>\n</ul>\n<ol start=\"4\">\n<li>Run the following commands:</li>\n</ol>\n<pre><code class=\"language-bash\"># Get your UUID#\nsudo blkid /dev/YOUR_DISK &gt; /tmp/blk.txt\n# Generate a hashed password\nmkpasswd -m yescrypt &gt; /tmp/pass.txt\n# Generate a rand # for `networking.hostId`\nhead -c4 /dev/urandom | xxd -p &gt; /tmp/rand.txt\n</code></pre>\n<ol start=\"5\">\n<li>\n<p>Edit <code>flake.nix</code>, <code>configuration.nix</code>, and replace the repos\n<code>hardware-configuration.nix</code> with your own.</p>\n</li>\n<li>\n<p>Add <code>neededForBoot</code> to the <code>home</code> and <code>persist</code> datasets in the generated\n<code>hardware-configuration.nix</code>.</p>\n</li>\n</ol>\n<p>Example:</p>\n<pre><code class=\"language-nix\">  fileSystems.\"/home\" = {\n    device = \"rpool/safe/home\";\n    fsType = \"zfs\";\n    neededForBoot = true;\n  };\n\n  fileSystems.\"/persist\" = {\n    device = \"rpool/safe/persist\";\n    fsType = \"zfs\";\n    neededForBoot = true;\n  };\n</code></pre>\n<ol start=\"7\">\n<li>Move the flake to <code>/mnt/etc/nixos/</code></li>\n</ol>\n<pre><code class=\"language-bash\">sudo mv ~/my-flake2 /mnt/etc/nixos/\n</code></pre>\n<ol start=\"8\">\n<li>Do a final check and install (change <code>host</code> to your host name)</li>\n</ol>\n<pre><code class=\"language-bash\">sudo nixos-install --flake /mnt/etc/nixos/myflake2#host\n</code></pre>\n<ul>\n<li>Read the comments, they let you know of requirements.</li>\n</ul>\n<ol start=\"9\">\n<li>\n<p>Reboot. I typically run <code>nixos-install</code> with the minimal requirements,\nreboot, and then configure my window manager/DE.</p>\n</li>\n<li>\n<p>After reboot, adjust permissions for your <code>$USER</code>:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mkdir -p /persist/home/$USER\n# Set ownership for the persistent home directory\nsudo chown -R 1000:100 /persist/home/$USER\n\n# Ensure the home dataset itself is accessible\nsudo chmod 755 /home\nsudo chmod 755 /persist/home\n# Test file, should be gone after reboot\nsudo touch /etc/rollback-canary\n</code></pre>\n<ol start=\"10\">\n<li>\n<p>Uncomment the import of the impermanence module in the <code>configuration.nix</code>.</p>\n</li>\n<li>\n<p>Reboot, then check:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">sudo ls /etc/rollback-canary\n</code></pre>\n<ul>\n<li>You should get an error:\n<code>\"/etc/rollback-canary\": No such file or directory (os error 2)</code></li>\n</ul>\n<hr />\n<h2>What gets Wiped vs. What Stays</h2>\n<p>What gets wiped?:</p>\n<p>Since we roll back (<code>rpool/local/root</code>):</p>\n<ul>\n<li>\n<p><code>/etc</code> (including system configs) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/var</code> (logs, databases, containers) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/root</code> (the root users home directory) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/usr</code> (though in NixOS this is mostly empty) -&gt; WIPED</p>\n</li>\n</ul>\n<p>What survives?:</p>\n<ul>\n<li>\n<p><code>/nix</code> (mounted from <code>rpool/local/nix</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/boot</code> (mounted from <code>rpool/local/boot</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/home</code> (mounted from <code>rpool/safe/home</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/persists</code> (mounted from <code>rpool/safe/persist</code>) -&gt; PERSISTS</p>\n</li>\n</ul>\n",
      "date_published": "2026-03-01T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/idiomatic_nix.html",
      "url": "https://saylesss88.github.io/idiomatic_nix.html",
      "title": "NixOS Containers",
      "content_html": "<h1>Idiomatic Nix</h1>\n<p>There are quite a few resources out there that share best practices, but no\nsingle unified place to find them all. I’m going to try to build on\n<a href=\"https://nix.dev/guides/best-practices\">nix.dev’s Best practices</a>, by doing some\nresearch as well as examining the code of some of the leaders in the NixOS\nworld. (Tweag, numtide, etc.)</p>\n<details>\n<summary> ✔️ mdbook-nix-repl for interactive code blocks </summary>\n<p>I’ve added a <code>flake.nix</code> to the <code>mdbook-nix-repl</code> repo, you can add it as a\nflake input:</p>\n<ol>\n<li><code>flake.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n\n    mdbook-nix-repl.url = \"github:saylesss88/mdbook-nix-repl?dir=server\";\n  };\n\n  outputs = { self, nixpkgs, mdbook-nix-repl, ... }: {\n    nixosConfigurations.magic = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      modules = [\n        ./configuration.nix\n\n        mdbook-nix-repl.nixosModules.default\n      ];\n    };\n  };\n}\n</code></pre>\n<ol start=\"2\">\n<li><code>configuration.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">{ pkgs, ... }:\n{\n  imports = [\n  ];\n\n  # This option is now provided by the module you imported from the flake\n  custom.nix-repl-server = {\n    enable = true;\n    port = 8080;\n    tokenFile = \"/etc/nix-repl-server.env\";\n  };\n}\n</code></pre>\n<ol start=\"3\">\n<li>Copy the value of <code>NIX_REPL_TOKEN</code> in <code>theme/index.hbs</code>, and add create file\n<code>/etc/nix-repl-server.env</code>:</li>\n</ol>\n<pre><code class=\"language-bash\"># Create the file with strict permissions (root read-only)\nsudo touch /etc/nix-repl-server.env\nsudo chmod 600 /etc/nix-repl-server.env\n\n# Edit it to add: NIX_REPL_TOKEN=your_token_from_index_hbs\nsudo vim /etc/nix-repl-server.env\n</code></pre>\n<p>Expected format:</p>\n<pre><code class=\"language-text\">NIX_REPL_TOKEN=9deb7efadb74b9e962e7911bb5caf3b3fef275a1b915b526\n</code></pre>\n<ol start=\"4\">\n<li>Rebuild, and the server will now be running at boot.</li>\n</ol>\n</details>\n<p>All the following examples are interactive, press play to see the result.(The\nfollowing examples come directly from <code>nix.dev</code>)</p>\n<pre><code class=\"language-nix\">rec {\n    a = 1;\n    b = a + 2;\n}\n</code></pre>\n<p>Use this instead:</p>\n<pre><code class=\"language-nix\">let\n  a = 1;\nin {\n    a = a;\n    b = a + 2;\n}\n</code></pre>\n<blockquote>\n<p>💡 TIP Self-reference can be achieved by explicitly naming the attribute set:</p>\n</blockquote>\n<pre><code class=\"language-nix\"> let\n   argset = {\n     a = 1;\n     b = argset.a + 2;\n  };\nin\n  argset\n</code></pre>\n<h2>Updating nested attribute sets</h2>\n<pre><code class=\"language-nix\">{ a = 1; b = 2; } // { b = 3; c = 4; }\n</code></pre>\n<p>Updates are shallow, names on the right take precidence:</p>\n<pre><code class=\"language-nix\">{ a = { b = 1; }; } // { a = { c = 3; }; }\n</code></pre>\n<pre><code class=\"language-nix\">let pkgs = import &lt;nixpkgs&gt; {}; in\npkgs.lib.recursiveUpdate { a = { b = 1; }; } { a = { c = 3;}; }\n</code></pre>\n",
      "date_published": "2026-01-30T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/secureboot_libvirt.html",
      "url": "https://saylesss88.github.io/nix/secureboot_libvirt.html",
      "title": "Secure Boot on Libvirt stack",
      "content_html": "<h1>Secure Boot in a Libvirt (KVM) VM with ZFS on LUKS Impermanence</h1>\n<h2>Initial VM Setup</h2>\n<p>When creating the VM in virt-manager:</p>\n<ol>\n<li>\n<p>Before clicking “Finish”, check the “Customize configuration before install”\nbox</p>\n</li>\n<li>\n<p>In the Overview section, change Firmware from BIOS to UEFI x86_64:\n<code>/usr/share/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2</code></p>\n</li>\n<li>\n<p>Proceed with the NixOS installation as normal. For lanzaboote to build\nsuccessfully, I had to pin it to nixpkgs <code>25.05</code>.</p>\n</li>\n</ol>\n<p><strong>Known Issue</strong>: After running <code>nixos-install</code> and rebooting, the SATA CDROM\nsource path may be cleared. If the VM fails to boot, manually reselect the NixOS\nISO in the SATA settings and reboot. ​</p>\n<hr />\n<h2>Configure Firmware for Custom Secure Boot Keys</h2>\n<p>The default configuration uses Microsoft’s pre-enrolled keys, which won’t trust\nyour custom-signed kernel. To enable custom key enrollment, you need to modify\nthe VM’s XML configuration. ​</p>\n<p>On your host, find your VM name:</p>\n<pre><code class=\"language-bash\">virsh -c qemu:///system list --all\n</code></pre>\n<p>Example Output:</p>\n<pre><code class=\"language-bash\"> Id   Name             State\n---------------------------------\n -    nixos-unstable   shut off\n -    nixos            shut off\n</code></pre>\n<p>Edit the VM configuration:</p>\n<pre><code class=\"language-bash\">virsh edit nixos-unstable\n</code></pre>\n<p>Make the following changes to the <code>&lt;os&gt;</code> section:</p>\n<ol>\n<li>Change the <code>enrolled-keys</code> feature from <code>yes</code> to <code>no</code>:</li>\n</ol>\n<pre><code class=\"language-xml\">&lt;feature enabled='no' name='enrolled-keys'/&gt;\n</code></pre>\n<ol start=\"2\">\n<li>Delete the explicit <code>&lt;loader&gt;</code> and <code>&lt;nvram&gt;</code> lines. These conflict with\nlibvirt’s firmware autoselection when using <code>enrolled-keys='no'</code>:</li>\n</ol>\n<pre><code class=\"language-xml\">&lt;!-- DELETE THESE TWO LINES --&gt;\n&lt;loader readonly='yes' secure='yes' type='pflash' format='qcow2'&gt;/usr/share/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2&lt;/loader&gt;\n&lt;nvram template='/usr/share/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2' templateFormat='qcow2' format='qcow2'&gt;/var/lib/libvirt/qemu/nvram/nixos-unstable_VARS.qcow2&lt;/nvram&gt;\n</code></pre>\n<p>Your final <code>&lt;os&gt;</code> section should look like this:</p>\n<pre><code class=\"language-xml\">&lt;os firmware='efi'&gt;\n  &lt;type arch='x86_64' machine='pc-q35-10.1'&gt;hvm&lt;/type&gt;\n  &lt;firmware&gt;\n    &lt;feature enabled='no' name='enrolled-keys'/&gt;\n    &lt;feature enabled='yes' name='secure-boot'/&gt;\n  &lt;/firmware&gt;\n  &lt;boot dev='hd'/&gt;\n&lt;/os&gt;\n</code></pre>\n<ol start=\"3\">\n<li>Add the <code>&lt;serial&gt;</code> tag as a best practice</li>\n</ol>\n<pre><code class=\"language-xml\">    &lt;disk type='file' device='disk'&gt;\n      &lt;driver name='qemu' type='qcow2' discard='unmap'/&gt;\n      &lt;source file='/var/lib/libvirt/images/nixos-unstable-1.qcow2'/&gt;\n      &lt;serial&gt;disk01&lt;/serial&gt;\n      &lt;target dev='vda' bus='virtio'/&gt;\n      &lt;address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/&gt;\n    &lt;/disk&gt;\n</code></pre>\n<ul>\n<li>This enables commands like: <code>zpool import -d /dev/disk/by-id rpool</code></li>\n</ul>\n<p>Save and exit the editor. Libvirt will now automatically create a new NVRAM file\nin Setup Mode (no keys enrolled).</p>\n<hr />\n<h2>NixOS Installation with ZFS on root with LUKS</h2>\n<ul>\n<li><a href=\"https://saylesss88.github.io/nix/encrypted_zfs.html\">ZFS on root with LUKS &amp; Impermanence</a></li>\n</ul>\n<p>Add the impermanence flake input. I also had to pin lanzaboote to nixpkgs\n<code>25.05</code> for lanzaboote to build successfully:</p>\n<pre><code class=\"language-nix\">inputs = {\n   impermanence.url = \"github:nix-community/impermanence\";\n   nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n   nixpkgs-stable.url = \"github:nixos/nixpkgs/nixos-25.05\";\n};\n</code></pre>\n<p>Add lanzaboote, if you don’t use <code>unstable</code>, you can obviously avoid the\noverlay:</p>\n<pre><code class=\"language-nix\">{ pkgs, lib, inputs, ... }: {\n# configuration.nix\nnixpkgs.overlays = [\n   (final: prev: {\n      lanzaboote = (inputs.nixpkgs-stable.legacyPackages.${pkgs.system}.lanzaboote or prev.lanzaboote)\n   })\n];\n\nenvironment.systemPackages = [ pkgs.sbctl ];\n\nboot.loader.systemd-boot.enable = lib.mkForce false;\n\nboot.lanzaboote = {\n  enable = true;\n  pkiBundle = \"/var/lib/sbctl\";\n};\n}\n</code></pre>\n<p>And <code>impermanence.nix</code>:</p>\n<pre><code class=\"language-nix\">{ inputs, lib, ... }: {\n   imports = [\n      inputs.impermanence.nixosModules.impermanence\n   ];\n   boot.initrd.postMountCommands = lib.mkAfter ''\n     zfs rollback -r rpool/local/root@blank\n   '';\n   environment.persistence.\"/persist\" = {\n      directories = [ \"/var/lib/sbctl\" \"/var/lib/nixos\" ];\n   };\n   fileSystems.\"/persist\" = {\n      device = \"rpool/safe/persist\";\n      fsType = \"zfs\";\n      neededForBoot = true;\n   };\n}\n</code></pre>\n<hr />\n<h2>Lanzaboote Installation</h2>\n<ul>\n<li><a href=\"https://saylesss88.github.io/installation/enc/lanzaboote.html\">Secure Boot with Lanzaboote</a></li>\n</ul>\n<hr />\n<h2>Enroll the Secure Boot Keys</h2>\n<p>After the XML changes, boot the VM and enter the firmware setup (press ESC\nduring boot).</p>\n<ol>\n<li>\n<p>Navigate to Device Manager → Secure Boot Configuration</p>\n</li>\n<li>\n<p>Switch to “Custom mode”, uncheck Attempt Secure Boot [ ], select “Reset\nSecure Boot Keys”, save with F10, and reboot.</p>\n</li>\n<li>\n<p>Enroll the keys: <code>sudo sbctl enroll-keys -m</code></p>\n</li>\n<li>\n<p>Reboot: After enrolling the keys and rebooting, the system will automatically\nbe placed in “Standard mode”, and Attempt Secure Boot [x] selected. You do\nnot need to re-enter firmware setup mode.</p>\n</li>\n<li>\n<p>Verify Secure Boot Status: <code>bootctl status</code></p>\n</li>\n</ol>\n",
      "date_published": "2026-01-19T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/encrypted_ZFS.html",
      "url": "https://saylesss88.github.io/installation/enc/encrypted_ZFS.html",
      "title": "ZFS with LUKS and Impermanence",
      "content_html": "<h1>ZFS with LUKS and Impermanence</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>I tested this on the libvirt stack with KVM, this should work on bare metal with\na few omissions.</p>\n<details>\n<summary> ✔️ SSH Method </summary>\n<p>This saves a ton of typing…</p>\n<ol>\n<li>\n<p>Boot the minimal ISO</p>\n</li>\n<li>\n<p>Set a password for the <code>nixos</code> user: <code>sudo passwd nixos</code></p>\n</li>\n<li>\n<p>Find the IP address: <code>ip a</code> (look for <code>etho</code> or <code>wlan0</code>)</p>\n</li>\n<li>\n<p>SSH in from your host or another machine: <code>ssh nixos@192.168.1.x</code></p>\n</li>\n</ol>\n<ul>\n<li><a href=\"https://github.com/saylesss88/my-flake2\">Starter Repo containing a flake and the configuration.nix from this chapter</a></li>\n</ul>\n<pre><code class=\"language-bash\">git clone https://github.com/saylesss88/my-flake2.git\n</code></pre>\n<p>I included a personally tested script that handles all of the steps up until you\nstart configuring the <code>configuration.nix</code>.</p>\n<pre><code class=\"language-bash\">cd my-flake2\nsudo chmod +x install.sh\nsudo ./install.sh\n</code></pre>\n<p>You will be prompted to enter your disk, just enter the disk, not anything else,\nlike this:</p>\n<pre><code class=\"language-bash\">vda\n# OR\nnvme0n1\n</code></pre>\n<p>After the first script completes, there is a <code>test.sh</code> script that ensures\neverything is in order.</p>\n<pre><code class=\"language-bash\">sudo chmod +x test.sh\nsudo ./test.sh\n</code></pre>\n</details>\n<h2>What is OpenZFS</h2>\n<p>ZFS is an advanced filesystem, originally developed by Sun Microsystems in 05.</p>\n<p>OpenZFS is a fork of the proprietary Oracle ZFS, it was forked over a decade\nago. Most of the code remains the same so you can check the\n<a href=\"https://docs.oracle.com/en/storage/zfs-storage/index.html\">Oracle website</a> for\ndocs and administration guides.</p>\n<p>ZFS is licensed under the\n<a href=\"https://en.wikipedia.org/wiki/CDDL\">Common Development and Distribution License</a>\n(CDDL). Because the CDDL is incompatible with the GPL,\n<a href=\"https://sfconservancy.org/blog/2016/feb/25/zfs-and-linux/\">it is not possible</a>\nfor ZFS to be included in the Linux Kernel. This requirement, however, does not\nprevent a native Linux kernel module from being developed and distributed by a\nthird party, as is the case with <a href=\"https://openzfs.org/\">OpenZFS</a> (previously\nnamed ZFS on Linux). –arch wiki</p>\n<h2>Comparison: OpenZFS Native Encryption vs. LUKS</h2>\n<details>\n<summary> ✔️ OpenZFS Native Encryption vs. LUKS </summary>\n<blockquote>\n<p>NOTE: This isn’t an attack on ZFS Native Encryption, I’m just presenting the\ninformation, the choice is yours. Unless you have a high threat model, ZFS\nnative encryption has many benefits such improved flexibility and\nauthentication.</p>\n</blockquote>\n<ul>\n<li>\n<p><strong>LUKS (Device Layer)</strong>: LUKS operates on the block device (like\n<code>/dev/nvme0n1</code>). It knows nothing about files, folders, or datasets. It just\nsees a stream of bytes and scrambles them.</p>\n<ul>\n<li>Everything on the partition is encrypted. The filesystem sits inside the\nencrypted container.</li>\n<li>With standard LUKS, an adversary can see you are using encryption (the LUKS\nheader is visible), but they cannot determine what filesystem (ZFS, ext4,\netc.) is inside. If you go a step further and use a detached header, the\nentire drive looks like random noise, providing plausible deniability that\nany data exists at all.</li>\n</ul>\n</li>\n<li>\n<p><strong>ZFS Native (Filesystem Layer)</strong>: ZFS encryption operates at the Dataset\n(Filesystem) level. It is aware of the structure of your data.</p>\n<ul>\n<li>Only the <strong>file blocks</strong> (the actual content of your files) and the <strong>file\nattributes</strong> (ACLs, permissions) within a specific dataset are encrypted.</li>\n</ul>\n</li>\n</ul>\n<p>No matter how careful you are with ZFS Native encryption, these cannot be\nhidden:</p>\n<ul>\n<li>\n<p><strong>The Structure</strong>: The fact that <code>dataset A</code> is a child of <code>dataset B</code></p>\n</li>\n<li>\n<p><strong>The Volume</strong>: The exact amount of disk space used by each dataset (in bytes)</p>\n</li>\n<li>\n<p><strong>The Activity</strong>: The fact that <em>something</em> changed at a specific time (via\nsnapshot creation or <code>used</code> property changing)</p>\n</li>\n</ul>\n<blockquote>\n<p>By using generic dataset names (e.g., <code>data/vol1</code> instead of\n<code>data/mistress_photos</code>) and automated snapshot schedules, you can strip the\nsemantic value from the exposed metadata. An attacker will see that you have\ndata and how much you have, but they won’t know what it is or which dataset is\nthe valuable one.</p>\n</blockquote>\n<p><strong>Sanitizing your ZFS usage (Mitigating risk)</strong>:</p>\n<p>By following a few simple best practices you can mitigate most, if not all of\nthe risk depending on the situation.</p>\n<ul>\n<li>\n<p>Use generic IDs for dataset names, instead of <code>tank/mistress_photos</code> use\n<code>tank/vol-A</code></p>\n</li>\n<li>\n<p>Automate your snapshots to prevent traffic analysis</p>\n</li>\n<li>\n<p>Use padding to mitigate size analysis, add dummy files to obfuscate the size\n(Can be tedious in ZFS). This is the hardest to mitigate, if this is a real\nthreat, use LUKS instead.</p>\n</li>\n</ul>\n<p>OpenZFS native encryption allows you to transparently encrypt data at rest\nwithin ZFS itself. It was initially released in May 2019, giving it much less\ntime to be scrutinized compared to LUKS (initial release in 2004).</p>\n<p>OpenZFS Encryption operates at the dataset layer, not the disk layer, which\ncreates several critical security gaps that full-disk encryption (FDE) solutions\nlike LUKS completely avoid.</p>\n<p>Unlike LUKS, which presents a “black box” of random noise to anyone without the\nkey, ZFS Native Encryption must leave certain structural elements visible to the\noperating system. This is a deliberate design choice that allows ZFS to perform\nmaintenance tasks (like scrubbing) on locked datasets without requiring the user\nto type in a password. (Depending on your threat model, this can be a deal\nbreaker).</p>\n<hr />\n<h2>Threat Example</h2>\n<p><strong>Example: Threat Model where ZFS Native Encryption falls short compared to\nLUKS</strong></p>\n<ol>\n<li>The “Pattern of Life” Attack (Timestamp Leaks)</li>\n</ol>\n<p><strong>The Leak</strong>: ZFS snapshot names and creation times are visible in plaintext\n(<code>zfs list -t snapshot</code>)</p>\n<p><strong>The Threat Model</strong>: An adversary monitoring your backups or seizing your drive\ncan build a profile of your behavior without decrypting a single byte.</p>\n<ul>\n<li>\n<p>If you claim to be asleep at 3AM, but <code>zfs list</code> shows snapshots being created\nor data changing size at 3:15 AM, your alibi is broken.</p>\n</li>\n<li>\n<p>Or, say a whistleblower contacts a journalist. An adversary seizes the\njournalist’s laptop. They can’t read the files, but they see a new dataset\ngrew by exactly 5GB at the exact time the leak occured. Correlation = Guilt.</p>\n</li>\n</ul>\n<p><strong>Compress &amp; Encrypt</strong>:</p>\n<p>It’s actually a common misconception that LUKS prevents compression before\nencryption, both methods are able to do this.</p>\n<ul>\n<li>\n<p>ZFS Native: <code>Data -&gt; Compress -&gt; Encrypt -&gt; Checksum -&gt; Write to Disk</code></p>\n</li>\n<li>\n<p>ZFS on LUKS: <code>Data -&gt; Compress -&gt; ZFS Write -&gt; LUKS Encrypt -&gt; Write to Disk</code></p>\n</li>\n</ul>\n<p>Both methods save disk space.</p>\n<p><strong>Integrety and Authentication</strong>:</p>\n<p>This is the strongest architectural argument for ZFS Native.</p>\n<ul>\n<li>ZFS Native: Uses AES-GCM (default) which is an Authenticated Encryption with\nAssociated Data (AEAD) mode. If a single bit is flipped on disk (maliciously\nor accidentally), ZFS refuses to return the bad data and reports a checksum\nerror.\n<ul>\n<li>NOTE: You can get integrity with LUKS2 with <code>dm-integrity</code>, but it incurs a\nmassive performance penalty and is considered experimental for production\nuse.</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h2>ZFS Native Encryption OR LUKS</h2>\n<p><strong>Use LUKS if</strong>:</p>\n<ul>\n<li>\n<p>You can’t afford to leak metadata like dataset names or snapshot timestamps.\nLUKS makes the drive look like random noise.</p>\n</li>\n<li>\n<p>You require rock-solid stability. The ZFS-on-LUKS stack is very\n“battle-tested”.</p>\n</li>\n</ul>\n<p><strong>Use ZFS Native Encryption if</strong>:</p>\n<ul>\n<li>\n<p>You have “Untrusted” Offsite Backups: This is a really nice feature! You can\n<code>zfs send -w</code> your data to a friend’s server or a cloud VM. The remote host\ncannot mount or read your data because they never have the key, but they can\nstill scrub the pool and verify the data integrity.</p>\n</li>\n<li>\n<p>You need granular control: ZFS Native Encryption allows you to for example,\nkeep your OS root unencrypted for fast booting/repair, but your <code>/home</code>\ndirectory encrypted.</p>\n</li>\n<li>\n<p>You run many VMs/Containers: You can create a new encrypted dataset for a\nspecific project/client without repartitioning or creating loopback files.</p>\n</li>\n<li>\n<p>You want to verify data authenticity.</p>\n</li>\n<li>\n<p><a href=\"https://arstechnica.com/gadgets/2021/06/a-quick-start-guide-to-openzfs-native-encryption/\">arsTechnica quick start to openzfs-native-encryption</a></p>\n</li>\n<li>\n<p><a href=\"https://discourse.practicalzfs.com/t/is-native-encryption-ready-for-production-use/532\">Practical ZFS is native encryption ready for production use?</a></p>\n</li>\n</ul>\n</details>\n<h2>Getting Started</h2>\n<p>When creating the VM, before clicking “Finish”, check the “Customize\nconfiguration before install” box and choose EFI Firmware &gt; BIOS. <strong>You will\nwaste a bunch of time if you forget to do this</strong>!</p>\n<ul>\n<li>I used <code>OVMF_CODE.fd</code> in my testing.</li>\n</ul>\n<p><strong>Format your disk</strong></p>\n<ol>\n<li>Partition &amp; Format</li>\n</ol>\n<pre><code class=\"language-bash\">sudo cfdisk /dev/vda\nsudo mkfs.fat -F32 /dev/vda1\n</code></pre>\n<ol start=\"2\">\n<li>Setup LUKS</li>\n</ol>\n<pre><code class=\"language-bash\">sudo cryptsetup luksFormat /dev/vda2\nsudo cryptsetup open /dev/vda2 cryptroot\n</code></pre>\n<ol start=\"3\">\n<li>Create zpool (Edited 2026-01-18 normalization=none)</li>\n</ol>\n<pre><code class=\"language-bash\">sudo zpool create \\\n  -o ashift=12 \\\n  -o autotrim=on \\\n  -O acltype=posixacl \\\n  -O canmount=off \\\n  -O compression=zstd \\\n  -O normalization=none \\\n  -O relatime=on \\\n  -O xattr=sa \\\n  -O dnodesize=auto \\\n  -O mountpoint=none \\\n  rpool /dev/mapper/cryptroot\n</code></pre>\n<ol start=\"5\">\n<li>Dataset Creation</li>\n</ol>\n<pre><code class=\"language-bash\"># root (ephemeral)\nsudo zfs create -p -o canmount=noauto -o mountpoint=legacy rpool/local/root\nsudo zfs snapshot rpool/local/root@blank\n\n# nix store\nsudo zfs create -p -o mountpoint=legacy rpool/local/nix\n\n# persistent data\nsudo zfs create -p -o mountpoint=legacy rpool/safe/home\nsudo zfs create -p -o mountpoint=legacy rpool/safe/persist\n</code></pre>\n<ul>\n<li><code>mountpoint=legacy</code> means that systemd will take care of the mounting</li>\n</ul>\n<ol start=\"6\">\n<li>Mounting</li>\n</ol>\n<pre><code class=\"language-bash\"># 1. Mount root first\nsudo mount -t zfs rpool/local/root /mnt\n\n# 2. Create directories\nsudo mkdir -p /mnt/{nix,home,persist,boot}\n\n# 3. Mount ESP directly to /boot (simpler and safer for systemd-boot)\nsudo mount -t vfat -o umask=0077 /dev/vda1 /mnt/boot\n\n# 4. Mount other ZFS datasets\nsudo mount -t zfs rpool/local/nix /mnt/nix\nsudo mount -t zfs rpool/safe/home /mnt/home\nsudo mount -t zfs rpool/safe/persist /mnt/persist\n</code></pre>\n<ol start=\"7\">\n<li>Configuration Prep</li>\n</ol>\n<pre><code class=\"language-bash\">sudo nixos-generate-config --root /mnt\n</code></pre>\n<pre><code class=\"language-bash\">export NIX_CONFIG='experimental-features = nix-command flakes'\nnix-shell -p helix\n</code></pre>\n<pre><code class=\"language-bash\">sudo blkid /dev/vda2\n# Copy the uuid\n</code></pre>\n<pre><code class=\"language-nix\"># configuration.nix\n boot.initrd.luks.devices = {\n     cryptroot = {\n       device = \"/dev/disk/by-uuid/uuid#\";\n       allowDiscards = true;\n       preLVM = true;\n     };\n   };\n</code></pre>\n<pre><code class=\"language-nix\">boot.initrd.luks.devices.\"cryptroot\".device = \"/dev/disk/by-uuid/&lt;UUID-OF-PARTITION-2&gt;\";\n</code></pre>\n<hr />\n<h2>Prep <code>configuration.nix</code></h2>\n<pre><code class=\"language-bash\">head -c4 /dev/urandom | xxd -p &gt; /tmp/rand.txt\n</code></pre>\n<p><strong>Create password file in a persistent location</strong>:</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /mnt/persist/etc/nixos-secrets/passwords\n\n #1) This is for `initialHashedPassword`\n #   Read this in with `:r /tmp/pass.txt`\nmkpasswd --method=yescrypt &gt; /tmp/pass.txt\n</code></pre>\n<p>After first reboot, the above files will be placed directly under <code>/persist/</code></p>\n<p>(Edited 2026-01-18 use <code>postMountCommands</code> &gt; <code>postResumeCommands</code>)</p>\n<pre><code class=\"language-nix\">{ config, lib, pkgs, ... }:\n\n{\n  # ------------------------------------------------------------------\n  # 1. Boot loader – systemd-boot (UEFI only)\n  # ------------------------------------------------------------------\n  boot.loader = {\n    systemd-boot = {\n      enable = true;\n      consoleMode = \"max\";\n      editor = false;\n    };\n    efi = {\n      canTouchEfiVariables = true;\n      efiSysMountPoint = \"/boot\";\n    };\n  };\n\n  # ------------------------------------------------------------------\n  # 2. ZFS support see: https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/index.html\n  # ------------------------------------------------------------------\n  boot.supportedFilesystems = [ \"zfs\" ];\n  boot.zfs.devNodes = \"/dev/\";       # Critical for VMs\n  # Not needed with LUKS\n  boot.zfs.requestEncryptionCredentials = false;\n  # systemd handles mounting\n  systemd.services.zfs-mount.enable = false;\n\n  services.zfs = {\n    autoScrub.enable = true;\n    # periodically runs `zpool trim`\n    trim.enable = true;\n    # autoSnapshot = true;\n  };\n\n  # ------------------------------------------------------------------\n  # 3. LUKS\n  # ------------------------------------------------------------------\n   boot.initrd.luks.devices = {\n     cryptroot = {\n    # replace uuid# with output of UUID # from `sudo blkid /dev/vda2`\n       device = \"/dev/disk/by-uuid/uuid#\";\n       allowDiscards = true;\n       preLVM = true;\n     };\n   };\n\n  # ------------------------------------------------------------------\n  # 4. Roll-back root to blank snapshot on **every** boot\n  # ------------------------------------------------------------------\n # Uncomment after first reboot\n # boot.initrd.postMountCommands = lib.mkAfter ''\n #   zfs rollback -r rpool/local/root@blank\n # '';\n\n  # ------------------------------------------------------------------\n  # 5. Basic system (root password, serial console for VM)\n  # ------------------------------------------------------------------\n  # Unique 8-hex hostId (run once in live ISO: head -c4 /dev/urandom | xxd -p)\n  networking.hostId = \"a1b2c3d4\";    # &lt;&lt;&lt;--- replace with your own value\n\n  users.users.root.initialPassword = \"changeme\";   # change after first login\n\n  boot.kernelParams = [ \"console=tty1\" ];\n\n  # ------------------------------------------------------------------\n  #  Users\n  # ------------------------------------------------------------------\n\n  users.mutableUsers = false;\n\n  # Change `your-user`\n  users.users.your-user = {\n    isNormalUser = true;\n    extraGroups = [ \"wheel\" ];\n    group = \"your-user\";\n    # :r /tmp/pass.txt:\n    initialHashedPassword = \"\";\n  };\n\n  # This enables `chown -R your-user:your-user`\n  users.groups.your-user = { };\n\n  # ------------------------------------------------------------------\n  #  (Optional) Helpful for recovery situations\n  # ------------------------------------------------------------------\n  # users.users.admin = {\n  #  isNormalUser = true;\n  #  description = \"admin account\";\n  #  extraGroups = [ \"wheel\" ];\n  #  group = \"admin\";\n    # initialHashedPassword = \"Output of `:r /tmp/pass.txt`\";\n # };\n\n # users.groups.admin = { };\n  # ------------------------------------------------------------------\n\n  # ------------------------------------------------------------------\n  # 6. (Optional) Enable SSH for post-install configuration\n  # ------------------------------------------------------------------\n  # services.openssh = {\n  #  enable = true;\n  #  settings.PermitRootLogin = \"yes\";\n  #};\n\n  # ------------------------------------------------------------------\n  # 7. Mark /persist as needed for boot\n  # ------------------------------------------------------------------\n  fileSystems.\"/persist\".neededForBoot = true;\n}\n</code></pre>\n<p>After reboot, you can uncomment:</p>\n<pre><code class=\"language-nix\">  boot.initrd.postMountCommands = lib.mkAfter ''\n    zfs rollback -r rpool/local/root@blank\n  '';\n</code></pre>\n<p>Uncomment the above script and test: (Don’t forget that the <code>/etc</code> directory\nwill be wiped, including your <code>configuration.nix</code> and\n<code>hardware-configuration.nix</code>!)</p>\n<p><strong>Configuration backup</strong></p>\n<pre><code class=\"language-bash\">sudo mkdir -p /persist/etc\nsudo cp /etc/nixos/hardware-configuration.nix /etc/nixos/configuration.nix /persist/etc/\n</code></pre>\n<p><strong>Rollback Test</strong></p>\n<pre><code class=\"language-bash\">sudo touch /etc/rollback-canary\nsudo reboot\n</code></pre>\n<p>If the rollback is working, <code>/etc/rollback-canary</code> should be gone after reboot\n(while things in <code>/persist</code> remain).</p>\n<hr />\n<h2>Adding a disk serial (libvirt XML)</h2>\n<p>NixOS ZFS boot support is broken for virtio drives without serial numbers.\nVirtio disks without serials don’t appear in /dev/disk/by-id, but ZFS boot logic\nonly tries to import pools from /dev/disk/by-id. The official OpenZFS NixOS\ndocumentation explicitly states: “If virtio is used as disk bus, power off the\nVM and set serial numbers for disk”</p>\n<p>In the <code>&lt;disk ...&gt;</code> block of your root disk add:</p>\n<p>Add to <code>.zshrc</code>:</p>\n<pre><code class=\"language-bash\">export LIBVIRT_DEFAULT_URI=\"qemu:///system\"\n</code></pre>\n<pre><code class=\"language-bash\">virsh list --all\nvirsh edit nixos-unstable\n</code></pre>\n<p>In the first <code>&lt;disk ... device='disk'&gt;</code> section (the one with target <code>dev='vda'</code>\n<code>bus='virtio'</code>), add a <code>&lt;serial&gt;</code> line, e.g.:</p>\n<pre><code class=\"language-xml\">&lt;disk type='file' device='disk'&gt;\n  &lt;driver name='qemu' type='qcow2' discard='unmap'/&gt;\n  &lt;source file='/var/lib/libvirt/images/nixos-unstable-1.qcow2' index='2'/&gt;\n  &lt;backingStore/&gt;\n  &lt;target dev='vda' bus='virtio'/&gt;\n  &lt;serial&gt;disk01&lt;/serial&gt;\n  &lt;alias name='virtio-disk0'/&gt;\n  ...\n&lt;/disk&gt;\n</code></pre>\n<hr />\n<h2>Issues I’ve Come Across</h2>\n<p><strong>sops-nix README</strong>: Explicitly warns: “If you are using Impermanence, the key\nused for secret decryption … must be in a persisted directory, loaded early\nenough during boot.” It specifically cites activation timing as the reason this\nfails.</p>\n<p>Typically the solution was to add <code>neededForUsers = true;</code> in your\n“password_hash” block but that isn’t working for this setup. (ZFS/LUKS/Imperm)</p>\n<p>I’ve been using <code>initialHashedPassword</code>, which bypasses the race entirely by\nbaking the salted hash into the store. This is all that I’ve found that works so\nfar…</p>\n<ul>\n<li>\n<p>Without adding the <code>&lt;serial&gt;disk01&lt;/serial&gt;</code> to the XML when you reboot, your\nsystem will hang before asking for your LUKS password.</p>\n</li>\n<li>\n<p>Without clicking “Customize configuration before install” box and choosing EFI\nFirmware instead of the default BIOS, <strong>you will not be able to boot at all</strong>.\nThis seems to be the case for all custom disk layouts with NixOS on the\nlibvirt stack.</p>\n</li>\n</ul>\n<hr />\n<h3>Resources</h3>\n<details>\n<summary> ✔️ Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://openzfs.github.io/openzfs-docs/\">openzfs-docs</a></p>\n</li>\n<li>\n<p><a href=\"https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/Root%20on%20ZFS.html\">openzfs-docs NixOS Root on ZFS</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/ZFS\">NixOS Wiki ZFS</a></p>\n</li>\n<li>\n<p><a href=\"https://klarasystems.com/articles/keeping-data-safe-with-openzfs-security-encryption-delegation/\">klarasystems OpenZFS: Security, Encryption, and Delegation Sept 2025</a></p>\n</li>\n<li>\n<p><a href=\"https://klarasystems.com/articles/improving-replication-security-with-openzfs-delegation/\">klarasystems Improving Replication Security with OpenZFS Delegation</a></p>\n</li>\n<li>\n<p><a href=\"https://forum.level1techs.com/t/zfs-guide-for-starters-and-advanced-users-concepts-pool-config-tuning-troubleshooting/196035\">ZFS Guide for starters &amp; advanced users</a></p>\n</li>\n<li>\n<p><a href=\"https://openzfs.org/wiki/System_Administration\">OpenZFS Sysem Administration</a></p>\n</li>\n<li>\n<p><a href=\"https://docs.oracle.com/cd/E19253-01/819-5461/\">Oracle Solaris ZFS Admin Guide</a></p>\n</li>\n<li>\n<p><a href=\"https://docs.freebsd.org/en/books/handbook/zfs/\">FreeBSD Handbook Chapter 22 The Z File System (ZFS)</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/ankek/awesome-zfs\">awesome-zfs</a></p>\n</li>\n<li>\n<p><a href=\"https://arstechnica.com/series/storage-fundamentals/\">arsTechnica Storage Fundamentals</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/jimsalterjrs/sanoid/\">sanoid</a></p>\n</li>\n<li>\n<p><a href=\"https://discourse.practicalzfs.com/\">Practical ZFS</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/openzfs/zfs\">openzfs/zfs GH Repo</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/openzfs/zfs/issues\">openzfs Issues</a></p>\n</li>\n<li>\n<p><a href=\"https://zfsonlinux.org/\">zfsonlinux.org</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.archlinux.org/title/ZFS\">arch wiki ZFS</a></p>\n</li>\n<li>\n<p><a href=\"https://tech-couch.com/post/btrfs-vs-zfs\">tech-couch Btrfs Vs ZFS</a></p>\n</li>\n<li>\n<p><a href=\"https://blog.purestorage.com/purely-educational/btrfs-vs-zfs/\">PureStorage btrfs vs zfs</a></p>\n</li>\n<li>\n<p><a href=\"https://www.cs.hmc.edu/~rhodes/cs134/readings/The%20Zettabyte%20File%20System.pdf\">The Zettabyte File System design docs</a></p>\n</li>\n<li>\n<p><a href=\"https://mrczntt.com/blog/understanding-zfs-the-zettabyte-file-system/\">Understanding ZFS, the Zettabyte File System</a></p>\n</li>\n<li>\n<p><a href=\"https://en.wikipedia.org/wiki/ZFS\">ZFS - Wikipedia</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2026-01-17T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/zfs_impermanence.html",
      "url": "https://saylesss88.github.io/nix/zfs_impermanence.html",
      "title": "ZFS Impermanence in a VM",
      "content_html": "<h1>ZFS Impermanence in a VM</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>Yet another blog post inspired by\n<a href=\"https://grahamc.com/blog/erase-your-darlings/\">erase your darlings</a></p>\n<p>I only tested this within a VM although with a few small tweaks it should work\non bare metal. I used the libvirtd stack with KVM for this.</p>\n<blockquote>\n<p>NOTE: This example doesn’t use encryption, it would be easy to add ZFS Native\nEncryption by changing the first <code>zpool</code> command. It’s good enough for most\npeople but does leak some metadata. I’ll add a LUKS example eventually which\nis more involved.</p>\n</blockquote>\n<ul>\n<li><a href=\"https://github.com/saylesss88/my-flake2\">Starter Repo containing a flake and the configuration.nix from this chapter</a></li>\n</ul>\n<pre><code class=\"language-bash\">git clone https://github.com/saylesss88/my-flake2.git\n</code></pre>\n<details>\n<summary> ✔️ SSH Method to enable copy-paste</summary>\n<ol>\n<li>\n<p>Boot the minimal ISO</p>\n</li>\n<li>\n<p>Set a password for the <code>nixos</code> user: <code>sudo passwd nixos</code></p>\n</li>\n<li>\n<p>Find the IP address: <code>ip a</code> (look for <code>eth0</code> or <code>wlan0</code>)</p>\n</li>\n<li>\n<p>SSH in from another machine: <code>ssh nixos@192.168.1.x</code></p>\n</li>\n<li>\n<p>Clone the repo and copy-paste commands from your browser to the terminal.</p>\n</li>\n</ol>\n</details>\n<details>\n<summary> ✔️ Multi-TTY Method (No extra Devices) </summary>\n<ol>\n<li>\n<p>Log in on the default TTY (usually Alt+F1).</p>\n</li>\n<li>\n<p>Switch to a second TTY by pressing Alt+F2.</p>\n</li>\n<li>\n<p>Log in again (user nixos, no password default).</p>\n</li>\n<li>\n<p>Clone your repo in TTY2: git clone https://github.com/your/repo.</p>\n</li>\n<li>\n<p>Open the README with a pager: less repo/README.md.</p>\n</li>\n<li>\n<p>Switch back to TTY1 (Alt+F1) to execute commands.</p>\n</li>\n<li>\n<p>Toggle back and forth (Alt+F2 / Alt+F1) to read and type.</p>\n</li>\n</ol>\n</details>\n<details>\n<summary> ✔️ tmux Method (Split Screen) </summary>\n<p>The minimal ISO includes <code>tmux</code> in the package set, but it’s not installed in\nthe environment by default.</p>\n<ol>\n<li>\n<p>Run: <code>nix run nixpkgs#tmux</code></p>\n</li>\n<li>\n<p>Once inside tmux, split the screen vertically: Press <strong>Ctrl+b</strong> then <strong>%</strong></p>\n</li>\n<li>\n<p>In the right pane, open the README: <code>less repo/README.md</code></p>\n</li>\n<li>\n<p>In the left pane, type the commands</p>\n</li>\n<li>\n<p>Switch panes with <strong>Ctrl+b</strong> then <strong>Left/Right Arrow</strong></p>\n</li>\n</ol>\n</details>\n<p>Start with a minimal ISO.</p>\n<p><a href=\"https://channels.nixos.org/nixos-25.11/latest-nixos-minimal-x86_64-linux.iso\">Download Minimal (64-bit Intel-AMD)</a></p>\n<p>Choose the LTS image, it comes with the <code>zfs</code> module enabled.</p>\n<p>I’ve also found that for my system it works best to switch the Video Model to\nVirtio, with 3D accelleration disabled (causes mouse inversion).</p>\n<p>When creating the VM, before clicking “Finish”, check the “Customize\nconfiguration before install” box and choose EFI Firmware &gt; BIOS. <strong>You will\nwaste a bunch of time if you forget to do this</strong>!</p>\n<ul>\n<li>I used <code>OVMF_CODE.fd</code> in my testing.</li>\n</ul>\n<p>Check out your layout:</p>\n<pre><code class=\"language-bash\">sudo fdisk -l\n</code></pre>\n<p>Format your disk:</p>\n<pre><code class=\"language-bash\">sudo cfdisk /dev/vda\n</code></pre>\n<p>Create a 1G <strong>EFI System</strong> first, then a <strong>Linux Filesystem</strong> with the remaining\nspace. I used (100G)</p>\n<p>For the following guide, you want <code>/dev/vda1</code> to be your <strong>EFI System</strong>\npartition, and <code>/dev/vda2</code> to be the <strong>Linux Filesystem</strong> partition.</p>\n<pre><code class=\"language-bash\">sudo fdisk -l\n</code></pre>\n<pre><code class=\"language-bash\">sudo mkfs.vfat -n EFI /dev/vda1\n</code></pre>\n<h2>Create Your ZFS Partitions</h2>\n<ol>\n<li>Create a zpool: (Edited 2026-01-18 normalization=none)</li>\n</ol>\n<pre><code class=\"language-bash\">zpool create \\\n  -o ashift=12 \\\n  -o autotrim=on \\\n  -O acltype=posixacl \\\n  -O canmount=off \\\n  -O dnodesize=auto \\\n  -O normalization=none \\\n  -O relatime=on \\\n  -O xattr=sa \\\n  -O mountpoint=none \\\n  rpool /dev/vda2\n</code></pre>\n<details>\n<summary> ZFS Native Encryption (Work in Progress) </summary>\n<pre><code class=\"language-bash\">zpool create -f \\\n  -o ashift=12 \\\n  -O encryption=aes-256-gcm \\\n  -O keyformat=passphrase \\\n  -O keylocation=prompt \\\n  -O mountpoint=none \\\n  -O acltype=posixacl \\\n  -O compression=lz4 \\\n  -O xattr=sa \\\n  rpool /dev/vda2\n</code></pre>\n<p>I just got impermanence working without encryption, I haven’t been able to test\nand iron out any quirks of this encryption method..</p>\n</details>\n<ol start=\"2\">\n<li>Create all datasets with parents (<code>-p</code>):</li>\n</ol>\n<pre><code class=\"language-bash\"># root (ephemeral – will be rolled back)\nzfs create -p -o canmount=noauto -o mountpoint=legacy rpool/local/root\n\n# blank snapshot (the “erase” target)\nzfs snapshot rpool/local/root@blank\n\nzfs create -p -o mountpoint=legacy rpool/local/boot\n# /nix – read-only store, must survive rollbacks\nzfs create -p -o mountpoint=legacy rpool/local/nix\n\n# persisted areas\nzfs create -p -o mountpoint=legacy rpool/safe/home\nzfs create -p -o mountpoint=legacy rpool/safe/persist\n</code></pre>\n<ol start=\"3\">\n<li>Mount everything under <code>/mnt</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">mount -t zfs rpool/local/root /mnt\n\nmkdir -p /mnt/{boot,boot/efi,nix,home,persist}\nmount -t vfat -o umask=0077 /dev/vda1 /mnt/boot/efi\nmount -t zfs rpool/local/nix   /mnt/nix\nmount -t zfs rpool/safe/home  /mnt/home\nmount -t zfs rpool/safe/persist /mnt/persist\n</code></pre>\n<blockquote>\n<p>Note: By placing your Nix flake in <code>/home/user/nixos-config</code> (which lives on\n<code>rpool/safe/home</code>), it persists naturally. You don’t need to add your\nconfiguration files to the <code>environment.persistence</code> module lists because the\nunderlying storage isn’t being wiped.</p>\n</blockquote>\n<ol start=\"4\">\n<li>Continue with the rest of the install</li>\n</ol>\n<pre><code class=\"language-bash\">nixos-generate-config --root /mnt\n# edit /mnt/etc/nixos/configuration.nix  (add ZFS + rollback + impermanence)\n</code></pre>\n<details>\n<summary> ✔️ Quick checklist: </summary>\n<p>Quick checklist to confirm that you’ve taken all of the necessary steps.</p>\n<pre><code class=\"language-bash\"># 1. pool\nzpool create -o ashift=12 -o autotrim=on -O acltype=posixacl -O canmount=off \\\n  -O dnodesize=auto -O normalization=formD -O relatime=on -O xattr=sa \\\n  -O mountpoint=none rpool /dev/vda2\n\n# 2. datasets + snapshot\nzfs create -p -o canmount=noauto -o mountpoint=legacy rpool/local/root\nzfs snapshot rpool/local/root@blank\nzfs create -p -o mountpoint=legacy rpool/local/nix\nzfs create -p -o mountpoint=legacy rpool/safe/home\nzfs create -p -o mountpoint=legacy rpool/safe/persist\n# add a /boot dataset\nzfs create -p -o mountpoint=legacy rpool/local/boot\n\n# 3. mounts\nmount -t zfs rpool/local/root /mnt\nmkdir -p /mnt/{boot,boot/efi,nix,home,persist}\n\n# /boot on ZFS\nmount -t zfs rpool/local/boot /mnt/boot\n\n# ESP on /boot/efi\nmount -t vfat -o umask=0077 /dev/vda1 /mnt/boot/efi\n\nmount -t zfs rpool/local/nix /mnt/nix\nmount -t zfs rpool/safe/home /mnt/home\nmount -t zfs rpool/safe/persist /mnt/persist\n</code></pre>\n</details>\n<h2>Prep <code>configuration.nix</code></h2>\n<pre><code class=\"language-bash\">head -c4 /dev/urandom | xxd -p &gt; /tmp/rand.txt\n</code></pre>\n<p><strong>Create password file in a persistent location</strong>:</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /mnt/persist/etc/nixos-secrets/passwords\n\n# 2) Create the password hash and write it to the persistent file\n# Replace \"your-password\" and \"your-user\"\nsudo sh -c 'mkpasswd -m yescrypt \"your-password\" &gt; /mnt/persist/etc/nixos-secrets/passwords/your-user'\n\n# 3) Lock down permissions\nsudo chown root:root /mnt/persist/etc/nixos-secrets/passwords/your-user\nsudo chmod 600 /mnt/persist/etc/nixos-secrets/passwords/your-user\n</code></pre>\n<ul>\n<li>After first reboot, the above files will be placed directly under <code>/persist/</code></li>\n</ul>\n<p>You will read <code>rand.txt</code> into the <code>configuration.nix</code> with <code>:r /tmp/rand.txt</code>.</p>\n<p>Edit the <code>/mnt/etc/nixos/configuration.nix</code> (Edited 2026-01-18 use\n<code>postMountCommands</code> instead of <code>postResumeCommands</code>) :</p>\n<pre><code class=\"language-nix\">{ config, lib, pkgs, ... }:\n\n{\n  # ------------------------------------------------------------------\n  # 1. Boot loader – systemd-boot (UEFI only)\n  # ------------------------------------------------------------------\n  boot.loader = {\n    systemd-boot = {\n      enable = true;\n      consoleMode = \"max\";           # Full 80×25 console in VM\n      editor = false;                # Security – no edit at boot\n    };\n    efi = {\n      canTouchEfiVariables = true;   # libvirt provides /sys/firmware/efi\n      efiSysMountPoint = \"/boot/efi\";    # Our 1 GiB FAT32 partition\n    };\n  };\n\n  # ------------------------------------------------------------------\n  # 2. ZFS support\n  # ------------------------------------------------------------------\n  boot.supportedFilesystems = [ \"zfs\" ];\n  boot.zfs.devNodes = \"/dev/\";       # Critical for VMs\n\n  # Unique 8-hex hostId (run once in live ISO: head -c4 /dev/urandom | xxd -p)\n  networking.hostId = \"a1b2c3d4\";    # &lt;&lt;&lt;--- replace with your own value\n\n  # ------------------------------------------------------------------\n  # 3. Roll-back root to blank snapshot on **every** boot\n  # ------------------------------------------------------------------\n# Uncomment after first reboot\n#  boot.initrd.postMountCommands = lib.mkAfter ''\n#    zfs rollback -r rpool/local/root@blank\n#  '';\n\n  # ------------------------------------------------------------------\n  # 4. Basic system (root password, serial console for VM)\n  # ------------------------------------------------------------------\n  users.users.root.initialPassword = \"changeme\";   # change after first login\n  boot.kernelParams = [ \"console=ttyS0,115200n8\" ];\n\n  users.mutableUsers = false;\n\n  users.users.your-user = {\n    isNormalUser = true;\n    extraGroups = [ \"wheel\" ];\n    group = \"your-user\";\n    # The location of `hashedPasswordFile` after first reboot\n    hashedPasswordFile = \"/persist/etc/nixos-secrets/passwords/your-user\";\n  };\n\n  # This enables `chown -R your-user:your-user`\n  users.groups.your-user = { };\n\n  # ------------------------------------------------------------------\n  # 5. (Optional) Enable SSH for post-install configuration\n  # ------------------------------------------------------------------\n  # services.openssh = {\n  #  enable = true;\n  #  settings.PermitRootLogin = \"yes\";\n  #};\n\n  # ------------------------------------------------------------------\n  # 6. Mark /persist as needed for boot\n  # ------------------------------------------------------------------\n  fileSystems.\"/persist\".neededForBoot = true;\n}\n</code></pre>\n<pre><code class=\"language-bash\">sudo nixos-install --root /mnt\n</code></pre>\n<pre><code class=\"language-bash\">reboot\n</code></pre>\n<p>Copy your system files to a persistent location before uncommenting the\nimpermanence script.</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /persist/etc\nsudo cp /etc/nixos/configuration.nix /etc/nixos/hardware-configuration.nix /persist/etc/\n</code></pre>\n<p>Now, you can uncomment this block:</p>\n<pre><code class=\"language-nix\">  boot.initrd.postMountCommands = lib.mkAfter ''\n    zfs rollback -r rpool/local/root@blank\n  '';\n</code></pre>\n<pre><code class=\"language-bash\">sudo touch /etc/rollback-canary\nsudo reboot\n</code></pre>\n<p>If the rollback is working, <code>/etc/rollback-canary</code> should be gone after reboot\n(while things in <code>/persist</code> remain).</p>\n<hr />\n<h2>What gets Wiped vs. What Stays</h2>\n<p><strong>What gets wiped?</strong>:</p>\n<p>Since we roll back <code>/</code>(<code>rpool/local/root</code>):</p>\n<ul>\n<li>\n<p><code>/etc</code> (including system configs) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/var</code> (logs, databases, containers) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/root</code> (the root users home directory) -&gt; WIPED</p>\n</li>\n<li>\n<p><code>/usr</code> (though in NixOS this is mostly empty) -&gt; WIPED</p>\n</li>\n</ul>\n<p><strong>What survives?</strong>:</p>\n<ul>\n<li>\n<p><code>/nix</code> (mounted from <code>rpool/local/nix</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/boot</code> (mounted from <code>rpool/local/boot</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/home</code> (mounted from <code>rpool/safe/home</code>) -&gt; PERSISTS</p>\n</li>\n<li>\n<p><code>/persists</code> (mounted from <code>rpool/safe/persist</code>) -&gt; PERSISTS</p>\n</li>\n</ul>\n<p><strong>Why this matters for secrets?</strong></p>\n<p><strong>SSH Host keys</strong> typically live in <code>/etc/ssh</code>. Since <code>/etc</code> is wiped, they\ndisappear. Store them in <code>/persist/etc/ssh</code> and tell NixOS to look there. (or\nsymlink them)</p>\n<p><strong>User Secrets</strong> (<code>~/.config/sops</code>): They live in <code>/home</code> so they’re safe.</p>\n<hr />\n<h2>Integrating into a Flake</h2>\n<p>After first reboot, I recommend setting up a flake in a persistent location such\nas <code>/home/your-user/flake</code>. Because subsequent reboots will wipe the <code>/etc</code>\ndirectory.</p>\n<ul>\n<li><a href=\"https://github.com/saylesss88/flakey\">Example Flake</a>, this is a WIP\nadaptation from another flake I had.</li>\n</ul>\n<pre><code class=\"language-bash\">sudo mkdir /imperm_test\necho \"This should be Gone after Reboot\" | sudo tee /imperm_test/testfile\nsudo ls -l /imperm_test/testfile # Verify the file exists\nsudo cat /imperm_test/testfile # Verify content\n</code></pre>\n<p>Reboot and check again:</p>\n<pre><code class=\"language-bash\">sudo ls -l /imperm_test/testfile # Verify the file no longer exists\nsudo cat /imperm_test/testfile # Verify content is missing\n</code></pre>\n<hr />\n<h2>Persisting SSH Keys</h2>\n<pre><code class=\"language-bash\">sudo mkdir -p /persist/etc/ssh\nsudo ssh-keygen -t ed25519 -f /persist/etc/ssh/ssh_host_ed25519_key -N \"\"\n</code></pre>\n<p>OR if you still have keys in <code>/etc/ssh</code> you want to keep just copy them to the\npersistent location:</p>\n<pre><code class=\"language-bash\">sudo cp /etc/ssh/ssh_host_ed25519_key* /persist/etc/ssh/\n</code></pre>\n<p><strong>Tell NixOS where to find them</strong></p>\n<pre><code class=\"language-nix\">services.openssh = {\n  hostKeys = [\n    {\n      path = \"/persist/etc/ssh/ssh_host_ed25519_key\";\n      type = ed25519;\n    }\n  ];\n}\n</code></pre>\n<p>After I initially get things working, I switch to <code>sops-nix</code>, the following\nguide works for this setup:\n<a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">sops-nix Guide</a></p>\n<hr />\n<h3>Resources</h3>\n<ul>\n<li>\n<p><a href=\"https://grahamc.com/blog/erase-your-darlings/\">erase-your-darlings</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/ZFS\">NixOS Wiki ZFS</a></p>\n</li>\n</ul>\n",
      "date_published": "2026-01-16T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/gpg-agent.html",
      "url": "https://saylesss88.github.io/nix/gpg-agent.html",
      "title": "GnuPG gpg-agent",
      "content_html": "<h1>GnuPG &amp; <code>gpg-agent</code> on NixOS</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<blockquote>\n<p>⚠️ <strong>SECURITY WARNING</strong>: This guide involves sensitive cryptographic material.\n<strong>Never share your private key or passphrase</strong>. Backup your keys and handle\nthem with extreme care.</p>\n</blockquote>\n<p><img src=\"https://saylesss88.github.io/../images/gnupg.png\" alt=\"GnuPG\" /></p>\n<h2>⚠️ gpg.fail (practical OpenPGP vulnerabilities) (Added on 2026-01-15)</h2>\n<p>The <a href=\"https://gpg.fail\">gpg.fail</a> is a write-up of real-world weaknesses in\nGPG/OpenPGP implementations and edge cases in the OpenPGP ecosystem—not the\nunderlying math of modern cryptography. ​ The core idea is that signature\nverification needs two things: correct cryptography and confidence that the data\nyou think was verified is actually the same data the verifier processed, which\ncan break down when formats are complex and tooling is permissive or ambiguous.</p>\n<h3>Why this matters even with good key hygiene</h3>\n<p>Most of the hardening in this guide (offline primary key, subkeys, strict\npermissions, agent separation) is still worth doing because it protects your\nprivate key material and reduces the blast radius if a workstation is\ncompromised.</p>\n<p>But that kind of key hygiene doesn’t automatically protect you from OpenPGP\n“sharp edges” like ambiguous parsing rules, weird message constructions, or\nimplementation bugs because those problems happen at the message/format/tooling\nlayer, not the key-storage layer.</p>\n<h3>How to avoid the sharp edges (actionable)</h3>\n<ul>\n<li>\n<p>Don’t treat “Good signature” as the end of the story. Treat verification as\n“verify + inspect what was verified,” especially if the output will be\nconsumed by other tools or humans who may misinterpret it. ​</p>\n</li>\n<li>\n<p>Prefer OpenPGP for narrow, well-understood tasks (verifying release artifacts,\nencrypting files) and be extra cautious when dealing with untrusted,\nattacker-controlled OpenPGP inputs that flow through multiple tools (mail\nclients, import pipelines, automation). ​</p>\n</li>\n<li>\n<p>Keep GnuPG and related tooling updated; gpg.fail covers vulnerabilities that\ninclude classic implementation issues and not just “protocol design” pitfalls.</p>\n</li>\n</ul>\n<h3>If you automate verification (important)</h3>\n<p>If you’re writing automation around signature verification, ensure your pipeline\nclearly distinguishes:</p>\n<ol>\n<li>\n<p>“signature cryptographically valid” from</p>\n</li>\n<li>\n<p>“the extracted/displayed message is exactly what was verified”. Some gpg.fail\nitems specifically call out cases where output does not make this distinction\nobvious enough to prevent misuse.</p>\n</li>\n</ol>\n<h3>NixOS/Home-Manager hardening can break GPG (common failure mode)</h3>\n<p>Some “hardening” choices can cause GPG to fail in ways that look mysterious but\nare just UI/agent integration problems (for example, <strong>No pinentry</strong> / “gpg\nfailed to sign the data” when the agent can’t prompt). If GPG signing/encryption\nsuddenly stops working, first confirm pinentry is configured correctly (via your\n<code>services.gpg-agent.pinentryPackage</code> or <code>gpg-agent.conf</code>) and restart the agent\nwith <code>gpgconf --kill gpg-agent</code> then <code>gpgconf --launch gpg-agent</code>.</p>\n<h2>🔑 Key Concepts</h2>\n<p><strong>GnuPG</strong> is a complete and free implementation of the OpenPGP standard. It\nallows you to encrypt and sign your data and communications, has a versatile key\nmanagement system, and access modules for many kinds of public key directories.\nGnuPG (GPG), is a command line tool for secure communication.</p>\n<p><strong>PGP (Pretty Good Privacy)</strong> and <strong>GPG (GNU Privacy Guard)</strong>. While distinct,\nthey are deeply interconnected and, for the rest of this section, I’ll use the\nterms interchangeably.</p>\n<p><strong>PGP</strong> was the original, groundbreaking software that brought robust public-key\ncryptography to the masses. It set the standard for secure email communication.\nHowever, PGP later became a commercial product.</p>\n<p>To provide a free and open-source alternative that anyone could use and inspect,\n<strong>GPG</strong> was created. Crucially, <strong>GPG</strong> is a complete implementation of the\nOpenPGP standard. This open standard acts as a universal language for encryption\nand digital signatures.</p>\n<p>GnuPG uses a more complex scheme in which a user has a primary keypair and then\nzero or more additional subordinate keypairs.</p>\n<p>Signing public keys with the corresponding private key is called <em>self-signing</em>,\nand a public key that has self-signed user IDs bound to it is called a\n<em>certificate</em>.</p>\n<p><strong>Web of Trust</strong>: Rather than validate every single key individually, you can\nrely on other factors such as if it has been signed by a key that you fully\ntrust or if it has been signed by three marginally trusted keys to validate\nkeys.</p>\n<p><code>gpg-agent</code> is a daemon to manage secret (private) keys independently from any\nprotocol. It is used as a backed for <code>gpg</code> and <code>gpgsm</code> as well as for a couple\nof other utilities. –<a href=\"https://man.cx/gpg-agent\">man gpg-agent</a></p>\n<p>There are numerous front-ends for gpg as well, i.e., GUI apps that simplify many\nof the commands and processes. Two that I touch on in this overview are\n<code>seahorse</code> and <code>kleopatra</code>.</p>\n<h3>Asymmetric Encryption (Public-Key cryptography)</h3>\n<p>E2ee requires that every sender and recipient does a one time preparation, which\ninvolves the generation of personal random numbers. Two such random numbers are\nnecessary, one will be called your secret key and another one will be called\nyour public key (together your <em>personal key</em>). These numbers are very big, they\nconsist of hundreds or thousands of digits.</p>\n<p>A message can be encrypted using the recipients public key and can only be\ndecrypted with the matching private key. In other words, if you exchange\n<strong>public keys</strong> with someone you both can encrypt messages that only the other\ncan decrypt with their own <strong>private key</strong>. <strong>You must never share the private\nkey or the private keys passphrase with anyone else</strong>.</p>\n<p><strong>What’s safe to share?</strong></p>\n<ul>\n<li>\n<p>Your public key (used to encrypt files and verify signatures)</p>\n</li>\n<li>\n<p>Your key ID (identifies your key, useful for sharing public keys or configs)</p>\n</li>\n<li>\n<p>Your keys fingerprint <code>gpg --fingerprint</code></p>\n</li>\n</ul>\n<p><strong>What must never be shared?</strong></p>\n<ul>\n<li>\n<p>Your private (secret) key, usually in your <code>~/.gnupg/private-keys-v1.d/</code>\ndirectory. Usually called your <em>private keyring</em>. <strong>Your main goal should be\nthe protection of your private key</strong>.</p>\n</li>\n<li>\n<p><strong>Your passphrase for your private key</strong>. Even if someone is able to somehow\nget your private key, they need to break the passphrase to access it\nunencrypted. <strong>Protect this passphrase</strong>!</p>\n</li>\n</ul>\n<p><strong>Best Practices</strong></p>\n<p>Don’t rely on the short KeyID, at least use long OpenPGP Key IDs (for example\n0xA1E6148633874A3D), they are 64 bits long and harder to spoof. Even better, use\nthe fingerprint.This is accomplished in the configuration with\n<code>keyid-format = \"0xlong\";</code>, and <code>with-fingerprint</code>.</p>\n<p>Always sign your public keys before you publish them to prevent man in the\nmiddle attacks and other modifications. When a subkey or userID is generated it\nis self-signed automatically, which is why you need to enter your password.</p>\n<p>Don’t blindly trust keys from keyservers. You should verify the full key\nfingerprint with the owner over the phone if possible.</p>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/Verifying_Software_Signatures\">Verifying software signatures</a></li>\n</ul>\n<p>Use a strong primary key, don’t use 1024-bit DSA, 1024-bit RSA, or SHA-1 for\nsigning they are no longer recommended.</p>\n<p>Choose an expiration date less than 2 years, you can add time if needed.\nRemember this date.</p>\n<p>Rotate your subkeys.</p>\n<p>Keep your primary key offline, this ensures that it can’t be stolen by an\nattacker allowing him to create new identities. We accomplish this by creating\nsubkeys and only adding the subkeys keygrip and the subkeys <code>default-key</code> to our\nconfiguration keeping the primary key out of it.</p>\n<p>Since we will be removing our primary key, even we won’t be able to create\nadditional keys so it’s important to think ahead and make all the keys you’ll\nneed. However, it is as easy as reimporting it to give yourself access again.</p>\n<p>Many of these best practices come from the following guide:</p>\n<ul>\n<li><a href=\"https://riseup.net/ru/security/message-security/openpgp/gpg-best-practices\">RiseUp gpg-best-practices</a></li>\n</ul>\n<hr />\n<p>Home Manager module with <code>gpg-agent</code>, <code>gnupg</code>, and <code>pinentry-gnome3</code>:</p>\n<pre><code class=\"language-nix\"># gpg-agent.nix\n{\n  config,\n  lib,\n  pkgs,\n  ...\n}: {\n  options = {\n    custom.pgp = {\n      enable = lib.mkEnableOption {\n        description = \"Enable PGP Gnupgp\";\n        default = false;\n      };\n    };\n  };\n\n  config = lib.mkIf config.custom.pgp.enable {\n    services = {\n      ## Enable gpg-agent with ssh support\n      gpg-agent = {\n        enable = true;\n        enableSshSupport = true;\n        enableZshIntegration = true;\n        # pinentry is a collection of simple PIN or passphrase dialogs used for\n        # password entry\n        pinentryPackage = pkgs.pinentry-qt;\n      };\n\n      ## We will put our keygrip here\n      gpg-agent.sshKeys = [];\n    };\n    home.packages = [pkgs.gnupg];\n    programs = {\n      gpg = {\n        ## Enable GnuPG\n        enable = true;\n\n        # homedir = \"/home/userName/.config/gnupg\";\n        settings = {\n          # Default/trusted key ID (helpful with throw-keyids)\n          # Example, you will put your own keyid here\n          # Use `gpg --list-keys`\n          # default-key = \"0x37ACBCDA569C5C44788\";\n          # trusted-key = \"0x37ACBCDA569C5C44788\";\n          # https://github.com/drduh/config/blob/master/gpg.conf\n          # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html\n          # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Esoteric-Options.html\n          # Some Best Practices, stronger algos etc\n          # Use AES256, 192, or 128 as cipher\n          personal-cipher-preferences = \"AES256 AES192 AES\";\n          # Use SHA512, 384, or 256 as digest\n          personal-digest-preferences = \"SHA512 SHA384 SHA256\";\n          # Use ZLIB, BZIP2, ZIP, or no compression\n          personal-compress-preferences = \"ZLIB BZIP2 ZIP Uncompressed\";\n          # Default preferences for new keys\n          default-preference-list = \"SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed\";\n          # SHA512 as digest to sign keys\n          cert-digest-algo = \"SHA512\";\n          # SHA512 as digest for symmetric ops\n          s2k-digest-algo = \"SHA512\";\n          # AES256 as cipher for symmetric ops\n          s2k-cipher-algo = \"AES256\";\n          # UTF-8 support for compatibility\n          charset = \"utf-8\";\n          # Show Unix timestamps\n          fixed-list-mode = \"\";\n          # No comments in signature\n          no-comments = \"\";\n          # No version in signature\n          no-emit-version = \"\";\n          # Disable banner\n          no-greeting = \"\";\n          # Long hexidecimal key format\n          keyid-format = \"0xlong\";\n          # Display UID validity\n          list-options = \"show-uid-validity\";\n          verify-options = \"show-uid-validity\";\n          # Display all keys and their fingerprints\n          with-fingerprint = \"\";\n          # Cross-certify subkeys are present and valid\n          require-cross-certification = \"\";\n          # Disable caching of passphrase for symmetrical ops\n          no-symkey-cache = \"\";\n          # Enable smartcard\n          # use-agent = \"\";\n        };\n      };\n    };\n  };\n}\n</code></pre>\n<blockquote>\n<p>🤔 Fun Fact: Elliot Alderson mentions encrypting Evil Corps files with 256 bit\nAES encryption ensuring that it’s impossible to break in <code>eps1.9_zer0.daY.avi</code>\nof Mr. Robot.</p>\n</blockquote>\n<ul>\n<li>\n<p>The default path is <code>~/.gnupg</code>, if you prefer placing it in the <code>~/.config</code>\ndirectory or elsewhere, uncomment the <code>homedir</code> line and change <code>userName</code> to\nyour username.</p>\n</li>\n<li>\n<p>I use sway so <code>pinentry-qt</code> works for me, there is also the following options\nfor this attribute:</p>\n</li>\n<li>\n<p><code>pinentry-tty</code></p>\n</li>\n<li>\n<p><code>pinentry-gnome3</code></p>\n</li>\n<li>\n<p><code>pinentry-gtk2</code></p>\n</li>\n</ul>\n<p>And more, research what you need and use the correct one.</p>\n<p><a href=\"https://search.nixos.org/packages?channel=unstable&amp;query=pinentry\">search.nixos.org pinentry</a></p>\n<p>Enable in your <code>home.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\"># home.nix\n# ... snip ...\nimports = [\n    ./gpg-agent.nix\n];\ncustom.pgp.enable = true;\n# ... snip ...\n</code></pre>\n<p><code>gpg --full-generate-key</code> can be used to generate a basic keypair.</p>\n<p><code>gpg --expert --full-generate-key</code> can be used for keys that require more\ncapabilities.</p>\n<blockquote>\n<p>❗ NOTE: We will first generate our GPG primary key that is required to\natleast have sign capabilities, we will then derive subkeys from said primary\nkey and use them for signing and encrypting. It is recommended to generate a\nrevoke certificate right after creating your primary key.</p>\n</blockquote>\n<p>To generate your gpg primary key you can do the following:</p>\n<pre><code class=\"language-bash\">gpg --full-generate-key\n</code></pre>\n<ul>\n<li>\n<p>Choose <code>(10) (sign only)</code></p>\n</li>\n<li>\n<p>Give it a name and description</p>\n</li>\n<li>\n<p>Give it an expiration date, 1y is common</p>\n</li>\n<li>\n<p>Use a strong passphrase or password</p>\n</li>\n<li>\n<p>Give it a comment, I typically add the date</p>\n</li>\n</ul>\n<p>If you see a warning about incorrect permissions, you can run the following:</p>\n<pre><code class=\"language-bash\">chmod 700 ~/.gnupg\nchmod 600 ~/.gnupg/*\n</code></pre>\n<p>Verify:</p>\n<pre><code class=\"language-bash\">ls -ld ~/.gnupg\n# Should show: drwx------\n\nls -l ~/.gnupg\n# Files should show: -rw-------\n</code></pre>\n<h3>Generate a Revocation Certificate</h3>\n<p><code>mykey</code> must be a key specifier, either the keyID of the primary keypair or any\npart of the user ID that identifies the keypair:</p>\n<p>Replace <code>mykeyID</code> with the keyID of your primary key and store the cert in a\nsafe place:</p>\n<pre><code class=\"language-bash\">gpg --output revoke.asc --gen-revoke mykeyID\nCreate a revocation certificate for this key? (y/N)\nPlease select the reason for the revocation:\n  0 = No reason specified\n  1 = Key has been compromised\n  2 = Key is superseded\n  3 = Key is no longer used\n  Q = Cancel\n(Probably you want to select 1 here)\nYour decision?\n</code></pre>\n<p>The certificate will be output to a file <code>revoke.asc</code>. If the <code>--output</code> is\nommitted, the result will be placed on stdout.</p>\n<p>Since it’s a short certificate, you can print a hardcopy and store it somewhere\nsafe. The cert shouldn’t be somewhere that others can access it since anyone\ncould publish the revoke cert and render the corresponding public key useless.</p>\n<p>To apply the revoke cert, import it:</p>\n<blockquote>\n<p>NOTE: Only import the <code>revoce.asc</code> if you want to revoke (i.e., make it not\nwork anymore)</p>\n</blockquote>\n<pre><code class=\"language-bash\">gpg --import revoke.asc\n# And optionally push the revoked key to public keyservers to notify others:\ngpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEYID\n</code></pre>\n<hr />\n<p>After fixing, run <code>gpg --list-keys --with-fingerprint</code>, which lists your public\nkeys:</p>\n<pre><code class=\"language-bash\"># Take note of your public key\ngpg --list-keys --with-fingerprint\n/home/jr/.gnupg/pubring.kbx\n---------------------------\npub   ed25519/0x095782A1B124AF15 2025-08-23 [SCA] [expires: 2026-08-23]\nKey fingerprint = 5908 9C5B FEC8 0D75 FCB0  E206 0958 82C1 A124 CF15\nuid                   [ultimate] Jr (08-23-25) &lt;sayls8@proton.me&gt;\n</code></pre>\n<ul>\n<li>Copy the KeyID, in this example it would be <code>0x095722B2A123CF15</code>. We will use\nit for the command below.</li>\n</ul>\n<p>The warning should be gone.</p>\n<p>Now we will generate 2 subkeys, 1 for encryption and 1 for authentication.</p>\n<pre><code class=\"language-bash\">gpg --expert --edit-key 0x095722B2A123CF15\n</code></pre>\n<p>When the screen opens, type <code>addkey</code></p>\n<p>Choose 11 (set your own capabilities) and add A (Authenticate) and type <code>save</code>\nto save and exit. Repeat this again and choose 12 ECC (encrypt only).</p>\n<blockquote>\n<p>❗ <code>gpg --edit-key</code> has many more capabilities, after launching type <code>help</code>.</p>\n</blockquote>\n<p><strong>Add Keygrip of Authenticate Subkey to <code>sshcontrol</code> for gpg-agent</strong></p>\n<pre><code class=\"language-bash\">gpg --list-secret-keys --with-keygrip --keyid-format LONG\n</code></pre>\n<p>Copy the keygrip of the subkey with Authenticate capabilities</p>\n<p>Add the keygrip number to your <code>gpg-agent.sshKeys</code> and rebuild, this adds an SSH\nkey to <code>gpg-agent</code>. This is for the SSH key functionality of <code>gpg-agent</code>, while\nthe key ID (<code>default-key</code>) is for GPG-specific operations like signing commits:</p>\n<pre><code class=\"language-nix\"># gpg-agent.nix\ngpg-agent.sshKeys = [\"6BD11826F3845BC222127FE3D22C92C91BB3FB32\"];\n</code></pre>\n<ul>\n<li>By itself, a keygrip cannot be used to reconstruct your private key. It’s\nderived from the public key material, not from the secret key itself so it’s\nsafe to version control. Don’t put your keygrip in a public repo if you don’t\nwant people to know you use that key for signing/authentication. It’s not a\nsecurity risk, but it leaks a tiny bit of metadata.</li>\n</ul>\n<p>The following article mentions the keygrip being computed from public elements\nof the key:</p>\n<ul>\n<li><a href=\"https://gnupg-users.gnupg.narkive.com/q5JtahdV/gpg-agent-what-is-a-keygrip\">gnupg-users what-is-a-keygrip</a></li>\n</ul>\n<p>Add the KeyId to your <code>gpg-agent.nix</code>, this declares your default-key to persist\nthrough rebuilds:</p>\n<p>Copy the public key of the same subkey with Authenticate capabilities you will\nsee something like <code>[SA]</code> next to it for Sign and Authenticate:</p>\n<pre><code class=\"language-nix\"># gpg-agent.nix\ngpg.settings = {\n    # Replace with your own Subkeys KeyID `gpg --list-keys --keyid-format LONG`\n    default-key = \"Ox37ACA569C5C44787\";\n    trusted-key = \"Ox37ACA569C5C44787\";\n};\n</code></pre>\n<p>This key should be signed automatically, ensure that it is:</p>\n<pre><code class=\"language-bash\">gpg --sign-key Ox37ACA569C5C44787\n</code></pre>\n<p>Rebuild, and check that everything is correct with:</p>\n<pre><code class=\"language-bash\">ssh-add -L\n# you should see something like:\nssh-ed25519 AABCC3NzaC1lZDI1NTE5ABBAIHyujgyCjjBTqIuFM3EMUSo6RGklmOXQW3uWRhWdJ1Mm (none)\n</code></pre>\n<ul>\n<li>Never version-control your private key files or <code>.gnupg</code> contents.</li>\n</ul>\n<p>Add the following to your shell config:</p>\n<pre><code class=\"language-bash\"># zsh.nix\n# ... snip ...\ninitContent = ''\n    export GPG_TTY=$(tty)\n    export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)\n    gpgconf --launch gpg-agent\n'';\n# ... snip ...\n</code></pre>\n<p>Rebuild and then restart <code>gpg-agent</code> if necessary:</p>\n<pre><code class=\"language-bash\">gpgconf --kill gpg-agent\ngpgconf --launch gpg-agent\n</code></pre>\n<p>Test, these should match:</p>\n<pre><code class=\"language-bash\">echo \"$SSH_AUTH_SOCK\"\n# output\n/run/user/1000/gnupg/d.wft5hcsny4qqq3g31c76534j/S.gpg-agent.ssh\n\ngpgconf --list-dirs agent-ssh-socket\n# output\n/run/user/1000/gnupg/d.wft5hcsny4qqq3g31c76834j/S.gpg-agent.ssh\n</code></pre>\n<pre><code class=\"language-bash\">ssh-add -L\n# Copy the entire following line:\nssh-ed25519 AABBC3NzaC1lZDI1NTE5AAAAIGXwhVokJ6cKgodYT+0+0ZrU0sBqMPPRDPJqFxqRtM+I (none)\n</code></pre>\n<ul>\n<li>Mine shows <code>(none)</code> because I left the comment field blank when creating the\nkey and doesn’t affect functionality.</li>\n</ul>\n<p>Then, in your server’s NixOS configuration (e.g., <code>configuration.nix</code>): Change\n<code>yourUser</code> to your username. This is how you grant access to a remote machine,\nand the public key from the GPG subkey is what’s added here, the output of\n<code>ssh-add -L</code>:</p>\n<pre><code class=\"language-nix\">users.users.yourUser = {\nopenssh = {\n  authorizedKeys.keys = [\n    # Replace with the output of `ssh-add -L`\n    \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXwhVokJ6cKgodYT+0+0ZrU0sBqMPPRDPJqFxqRtM+I (none)\"\n  ];\n};\n};\n</code></pre>\n<blockquote>\n<p>❗ NOTE: Only the <strong>public</strong> key goes here, it’s safe to commit to version\ncontrol. If you prefer not to hardcode it in the config, you can reference it\nfrom a <code>.pub</code> file in your repo and read it with\n<code>builtins.readFile ./mykey.pub</code></p>\n</blockquote>\n<p>Rebuild your system and test an SSH connection into the server:</p>\n<pre><code class=\"language-bash\">ssh -p &lt;your-port&gt; user@hostname\n</code></pre>\n<ul>\n<li><code>&lt;your-port&gt;</code> is often <code>22</code> so it would be something like:</li>\n</ul>\n<pre><code class=\"language-bash\">ssh -p 22 bill@xps\n</code></pre>\n<p>Once you successfully sign in to SSH, it will ask you if you’re sure you trust\nthe remote server’s SSH host key. Once you type <code>yes</code>, it will automatically be\nused for tasks such as file decryption.</p>\n<h3>Remove and Store your Primary Key offline</h3>\n<blockquote>\n<p>❗ NOTE: After you remove your primary key, you will no longer be able to\nderive subkeys from it or sign keys unless you re-import it.</p>\n</blockquote>\n<pre><code class=\"language-bash\"># extract the primary key\ngpg -a --export-secret-key sayls8@proton.me &gt; secret_key\n# extract the subkeys, which we will reimport later\ngpg -a --export-secret-subkeys sayls8@proton.me &gt; secret_subkeys.gpg\n# delete the secret keys from the keyring, so only subkeys are left\ngpg --delete-secret-keys sayls8@proton.me\nDelete this key from the keyring? (y/N) y\nThis is a secret key! - really delete? (y/N) y\n# reimport the subkeys\ngpg --import secret_subkeys.gpg\n# verify everything is in order\ngpg --list-secret-keys\n# remove the subkeys from disk\nrm secret_subkeys.gpg\n</code></pre>\n<p>I recommend also keeping a <code>.gpg</code> version to make it easy to re-import your\nprimary key: <code>gpg --export-secret-keys --armor --output private-key-bak.gpg</code></p>\n<p>Then store <code>secret_key</code> on an encrypted USB drive or somewhere offline. If you\nwant to protect it for now, you can just use the encryption subkey that we\ncreated to encrypt <code>secret_key</code> with a passphrase:</p>\n<pre><code class=\"language-bash\">gpg --list-keys --keyid-format LONG\n</code></pre>\n<p>Copy the KeyID of the subkey with encrypt capabilities for the following\ncommand:</p>\n<pre><code class=\"language-bash\"># Encrypting your secret key for yourself\ngpg --encrypt --recipient Ox37ACA569C5C44787 secret_key\n</code></pre>\n<p>You can check that the secret key material is missing with\n<code>gpg --list-secret-keys</code>, you should see <code>sec#</code> instead of <code>sec</code>.</p>\n<pre><code class=\"language-bash\">gpg --list-secret-keys\n# Output:\nsec#  ed25519/0x\n# ...snip...\n</code></pre>\n<p>The above set of commands are from the\n<a href=\"https://riseup.net/ru/security/message-security/openpgp/gpg-best-practices#keep-your-primary-key-entirely-offline\">RiseUp Keep your primary key offline</a></p>\n<h2>Add your GPG Key to GitHub</h2>\n<p>Plug your own public key from <code>gpg --list-keys</code> in the following command:</p>\n<pre><code class=\"language-bash\">gpg --armor --export &lt;Public-Key&gt;\n</code></pre>\n<p>Copy the entire block from <code>-----BEGIN PGP PUBLIC KEY BLOCK-----</code> to\n<code>-----END PGP PUBLIC KEY BLOCK-----</code></p>\n<blockquote>\n<p>❗ You can also paste the above block into a public keyserver such as\n<code>keys.openpgp.org</code>. This allows others to find and use your key to encrypt\nmessages or verify your signatures. Many tools and users rely on public key\nservers to fetch keys automatically. You can also publish your revocation\ncertificates, which help others know if your key is compromised or revoked.\nThis can be a privacy concern as key servers publish (and keep) associated\nuser IDs and metadata linked to your key, such as your email.</p>\n</blockquote>\n<p>It’s the same process as adding an SSH key, Go to Settings, SSH and GPG keys,\n<code>New GPG key</code> and your all set.</p>\n<h3>Sign your Commits for Git</h3>\n<pre><code class=\"language-nix\"># git.nix\n{...}: {\n    programs.git = {\n        enable = true;\n      extraConfig = {\n          commit.gpgsign = true;\n          user.signingkey = \"0x0666C1A265F156\"\n      };\n    };\n}\n</code></pre>\n<p>After this, you will be prompted for your Private Keys password on every commit.</p>\n<p>If you look at your commits on GitHub, after adding the GPG key and the above\nsettings to your git setup it will show your commits are <code>Verified</code>.</p>\n<h3>Backing up Your Keys</h3>\n<pre><code class=\"language-bash\">gpg --export-secret-keys --armor --output my-private-key-backup.gpg\n</code></pre>\n<p>Your private keys will be encrypted with a passphrase into a .gpg file. Store\nthis backup in a secure location line an encrypted USB drive. This can prevent\nyou from losing access to your keys in the case of disk failure or accidents.</p>\n<p>You can export your public keys and publish them publicly if you choose:</p>\n<pre><code class=\"language-bash\">gpg --export --armor --output my-public-keys.gpg\n</code></pre>\n<p>Now if your keys ever get lost or corrupted, you can import these backups.</p>\n<h2>Encrypt a File with PGP</h2>\n<p>The easy way to do this is with an app like Kleopatra, available as\n<code>pkgs.kdePackages.kleopatra</code>. Kleopatra will automatically recognize your gpg\nkeys and enable you to easily encrypt messages by clicking the Notepad, typing\nyour message and clicking <code>Sign/Encrypt Notepad</code>. You can also choose to encrypt\nthe message with a password, where anyone that has the password can read the\nmessage.</p>\n<p>Using the above and below methods enable you to encrypt any message for\nbasically any service and just copy past the encrypted text into the service for\nadded privacy.</p>\n<p>Encrypting a whole directory is a bit more involved and requires using\ncompression.</p>\n<h3>List your keys and get the key ID</h3>\n<pre><code class=\"language-bash\">gpg --list-keys --keyid-format LONG\n</code></pre>\n<p>Example output, don’t use RSA keys:</p>\n<pre><code class=\"language-bash\">pub   rsa4096/ABCDEF1234567890 2024-01-01 [SC]\nuid           [ultimate] Your Name &lt;you@example.com&gt;\nsub   rsa4096/1234567890ABCDEF 2024-01-01 [E]\n</code></pre>\n<ul>\n<li>\n<p>Notice the <code>sub</code> and the <code>[E]</code> for the subkey with encrypt capabilities.</p>\n</li>\n<li>\n<p>The part after the slash on the <code>pub</code> line is your key ID (<code>ABCDEF1234567890</code>\nin the example)</p>\n</li>\n<li>\n<p>You can also use your email or name to refer to the key in most commands.</p>\n</li>\n</ul>\n<h3>Encrypt a file</h3>\n<p>In order to encrypt a document you must have the public keys of the intended\nrecipients.</p>\n<pre><code class=\"language-bash\">echo \"This file will be encrypted\" &gt; file.txt\n</code></pre>\n<p>Encrypting for yourself (using a key ID as recipient):</p>\n<pre><code class=\"language-bash\">gpg --encrypt --recipient ABCDEF1234567890 file.txt\n</code></pre>\n<pre><code class=\"language-bash\">ls\n│  7 │ file.txt            │ file │     28 B │ now           │\n│  8 │ file.txt.gpg        │ file │    191 B │ now           │\n</code></pre>\n<p>Encrypting for someone else with their email (public key identifier):</p>\n<pre><code class=\"language-bash\">gpg --output file.gpg --encrypt --recipient jake@proton.me file.txt\n</code></pre>\n<pre><code class=\"language-bash\">ls\nfile.txt\nfile.gpg\n</code></pre>\n<p><code>gpg --encrypt</code> doesn’t modify the original file. It creates a new encrypted\nfile by default with <code>gpg</code> amended to the filename.</p>\n<pre><code class=\"language-bash\">gpg --decrypt file.txt.gpg\ngpg: encrypted with cv25519 key, ID 0x4AC131B80CEC833E, created 2025-07-31\n      \"GPG Key &lt;sayls8@proton.me&gt;\"\nThis file will be encrypted\n</code></pre>\n<p>Or, to save the decrypted text to a file:</p>\n<pre><code class=\"language-bash\">gpg --output decrypted_file.txt --decrypt file.txt.gpg\ncat decrypted_file.txt\n# Output\nFile: decrypted.txt\nThis file will be encrypted\n</code></pre>\n<ul>\n<li>You will be asked for the passphrase you used when creating the key in order\nto decrypt the file.</li>\n</ul>\n<h3>Sign and Verify Signatures</h3>\n<p>When you sign a document it is certified and timestamped. If after the doc is\nsigned, if the doc is further modified in any way the verification of the\nsignature will fail.</p>\n<p>A signature is created using the private key of the signer, and verified with\nthe corresponding public key. For example, to verify Jakes signature you would\nuse Jake’s public key to see that the work indeed came from him and hasn’t been\nmodified since.</p>\n<p>To sign the above <code>file.txt</code>:</p>\n<pre><code class=\"language-bash\">gpg output doc.sig --sign file.txt\n# To clearsign use:\ngpg --clearsign file.txt\n# For a detached signature use:\ngpg --output doc.sig --detach-sig file.txt\n</code></pre>\n<p>You will be prompted for your passphrase.</p>\n<p>You can either check the signature or check the signature and recover the\noriginal document.</p>\n<pre><code class=\"language-bash\"># To only check:\ngpg --output doc --verify doc.sig\n# To verify and extract the document\ngpg --output doc --decrypt doc.sig\n</code></pre>\n<h2>Email Encryption</h2>\n<p>Email is inherently insecure, and email-based attacks remain one of the top\nvectors for data breaches. Encrypting your email protects your privacy by\nensuring that only the intended recipient can read it. Encrypting your emails\nwith PGP provides valuable security benefits but also has inherent limitations\nthat prevent it from being considered truly “secure communication” by modern\nstandards.</p>\n<ul>\n<li><a href=\"https://emailselfdefense.fsf.org/en/\">Email Self-Defense</a></li>\n</ul>\n<p>What its Good for:</p>\n<ul>\n<li>\n<p>Confidentiality, it prevents unauthorized third parties (like email providers\nor network eavesdroppers) from reading your email content.</p>\n</li>\n<li>\n<p>Integrity and authenticity: Digital signatures verify that the email genuinely\ncame from the claimed sender and hasn’t been altered in transit.</p>\n</li>\n<li>\n<p>Long-term confidentiality: Encrypted emails stored on servers or devices\nremain protected even if the storage is later compromised. With companies like\nGmail giving you a “free” account, that usually means that you are the product\nand you should tread lightly.</p>\n</li>\n</ul>\n<p><strong>To securely communicate with someone never use email, use a dedicated service\nsuch as Threema, Signal or Brair.</strong> It’s hard to recommend any messaging service\nat the moment, always do your own research and stay informed on the companies\npolicies. Signal has taken some heat for the way it has implemented it’s\nMobileCoin. The biggest issue I’ve faced is getting other people to care or use\nthe same e2ee app.</p>\n<p>With Thunderbird you can go to settings, Privacy and Security, and scroll to the\nbottom where it says “End to End Encryption”, Click the Settings tab there,\nfinally click End-To-End Encryption on the left.</p>\n<p>From there, you can click <code>+ Add Key</code> next to your email address and either\ngenerate a new key through Thunderbird. If you use this, choose the Curve\nprotocol or whatever isn’t RSA.</p>\n<p>Or import your own key which is definitely more secure since you’re not trusting\nsomeone else with your private key:</p>\n<pre><code class=\"language-bash\">gpg --export --armor sayls8@gmail.com &gt; publickey.asc\n</code></pre>\n<p>Then select <code>+ Add Key</code> and choose import your own, this didn’t work for me.\nWhat did work was to start composing an email and click on the <code>OpenPGP</code> button,\nGo to <code>Key Manager</code>, <code>File</code>, <code>Import Public Key from a File</code> and choose your\n<code>publickey.asc</code>. This way, only you have access to your private key.</p>\n<ul>\n<li><a href=\"https://support.mozilla.org/en-US/kb/introduction-to-e2e-encryption#w_how-e2ee-with-openpgp-works-in-general\">How e2ee with OpenPGP works in general</a></li>\n</ul>\n<p><strong>Import your recipient’s public key</strong></p>\n<p>When you start composing an email, you’ll see that you need to resolve key\nissues if you don’t already have the recipients public key. Click <code>Resolve</code>, and\neither Discover Public Keys Online… or Import Public Keys From File…</p>\n<p>Thunderbird has the option to use the OpenPGP Key Manager to view or manage\npublic keys of your correspondents.</p>\n<p>If you’re sending encrypted emails to someone you’ll need their public key,\nthere are a few methods of doing this just ensure you verify the Fingerprint\nwith the person your talking to.</p>\n<p>Exporting your public key means creating a copy of the public part of your\ncryptographic key pair that you can share with others.</p>\n<p>For example, say that Jake wants to send you his public key. First he has to\nexport his key:</p>\n<pre><code class=\"language-bash\">gpg --output jake.gpg --export jake@proton.me\n</code></pre>\n<p>The key is exported in binary format, to export in ASCII-armored format use:</p>\n<pre><code class=\"language-bash\">gpg --armor --export jake@proton.me\n</code></pre>\n<p>Now, once he sends this to you, you’ll need to import and validate it:</p>\n<pre><code class=\"language-bash\">gpg --import jake.gpg\n</code></pre>\n<p>Once the key is imported it should be validated. If you need to validate a key\nmanually, it is done by verifying the key’s fingerprint and then signing the key\nto certify it as a valid key.</p>\n<p>Check that it exists in your keychain:</p>\n<pre><code class=\"language-bash\">gpg --list-keys\n</code></pre>\n<p>You should see Jakes key in the above list.</p>\n<p>To certify the key you need to edit it:</p>\n<pre><code class=\"language-bash\">gpg --edit-key jake@proton.me\n# List the fingerprint\ngpg&gt; fpr\n# once the fingerprint is verified with the owner, sign it\ngpg&gt; sign\n# once signed, you can check the key to list signatures on it\ngpg&gt; check\n</code></pre>\n<blockquote>\n<p>❗ NOTE: You can use PGP to encrypt any message and paste it into <strong>any</strong>\nsoftware and send it. As long as only you and your recipient are the only\npeople to have the private keys, you will be the only people able to decrypt\nthe messages. Implementing this correctly is a good way to stop government\nmass surveillance.</p>\n</blockquote>\n<h2>Make your Public Key Highly Available</h2>\n<p>You should always make sure that you sign your public key before you publish it.\nWhen you distribute your public key, you’re distributing the public components\nof your master and subkeys as well as the user IDs. If unsigned, this is a\nsecurity risk because it’s possible for an attacker to tamper with it. The\npublic key can be modified by adding or substituting keys, or changing user IDs.</p>\n<p>Signing the keys provides a web of trust, only the corresponding public key can\nbe used to verify the signature and ensure it hasn’t been modified. Since we are\nalready only using subkeys for public keys, they are automatically self-signed.</p>\n<pre><code class=\"language-bash\">gpg --output ~/mygpg.key --armor --export your_email@address.com\n</code></pre>\n<p>You can then send this file to the other party.</p>\n<p>You can also use the GPG interface to upload your key to a key server:</p>\n<pre><code class=\"language-bash\">gpg --list-keys your_email@address.com\n</code></pre>\n<p>Copy the key ID for the following command, remember its on the <code>pub</code> line after\nthe <code>/</code>.</p>\n<pre><code class=\"language-bash\">gpg --send-keys --keyserver pgp.mit.edu key_id\n</code></pre>\n<p>The key will be uploaded to the server and likely be distributed to other key\nservers around the world. This is why expiration dates are important, if your\nkey is lost or stolen, the damage window is limited to the expiration period.\nAlso remember, you can add more time even after the key has expired.</p>\n<h3>Example: Verifying Arch Linux Download</h3>\n<details>\n<summary>\n<p>✔️ Click to Expand Example of verifying and signing the archlinux public key</p>\n</summary>\n<p>First, download both the arch <code>.iso</code> and <code>.sig</code> files.</p>\n<p>I tried a few different methods from <a href=\"https://archlinux.org/download/#checksums\">https://archlinux.org/download/#checksums</a>\nand the easiest by far was using Sequoia available in Nixpkgs as\n<code>pkgs.sequoia-sq</code>:</p>\n<p>Download the archlinux public key:</p>\n<pre><code class=\"language-bash\">sq network wkd search pierre@archlinux.org --output release-key.pgp\n\nFound 2 certificates related to the query:\n\n - 3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C\n   - Pierre Schmitz &lt;pierre@archlinux.org&gt; (UNAUTHENTICATED)\n   - created 2022-10-31 09:11:51 UTC\n   - found via: WKD\n\n - 4AA4767BBC9C4B1D18AE28B77F2D434B9741E8AC\n   - Pierre Schmitz &lt;pierre@archlinux.de&gt; (UNAUTHENTICATED)\n   - created 2011-04-10 09:35:33 UTC\n   - found via: WKD\n\nHint: To extract a particular certificate from release-key.pgp, use any of:\n\n  $ sq cert export --keyring=release-key.pgp --cert=3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C\n\n  $ sq cert export --keyring=release-key.pgp --cert=4AA4767BBC9C4B1D18AE28B77F2D434B9741E8AC\n</code></pre>\n<p>Export the chosen key to a <code>.pgp</code> file:</p>\n<pre><code class=\"language-bash\">sq cert export --keyring=release-key.pgp --cert=3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C &gt; pierre-archlinux.pgp\n</code></pre>\n<p>Import into your keychain:</p>\n<pre><code class=\"language-bash\"> gpg --import pierre-archlinux.pgp\ngpg: key 0x76A5EF9054449A5C: 9 signatures not checked due to missing keys\ngpg: key 0x76A5EF9054449A5C: public key \"Pierre Schmitz &lt;pierre@archlinux.org&gt;\" imported\ngpg: Total number processed: 1\ngpg:               imported: 1\ngpg: marginals needed: 3  completes needed: 1  trust model: pgp\ngpg: depth: 0  valid:   3  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 3u\ngpg: next trustdb check due at 2026-08-23\n</code></pre>\n<p>Now, you should see <code>&lt;pierre@archlinux.org&gt;</code> and his keys when you run\n<code>gpg --list-keys</code></p>\n<p>Finally, verify the signature:</p>\n<pre><code class=\"language-bash\">sq verify --signer-file release-key.pgp --signature-file archlinux-2025.08.01-x86_64.iso.sig archlinux-2025.08.01-x86_64.iso\nAuthenticated signature made by 3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C (Pierre Schmitz &lt;pierre@archlinux.org&gt;)\n\n1 authenticated signature.\n</code></pre>\n<p>This shows that the signature was made by the key with the ID\n<code>3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C</code> (Pierre Schmitz).</p>\n<p>GPG authenticated that the signature is valid and that the key used to sign is\ntrusted in our keyring.</p>\n<p>1 authenticated signature confirms the files integrity and authenticity.</p>\n<p>We have successfully verified that the file was signed by Pierr’s official Arch\nLinux key and has not been tampered with.</p>\n<p>Since the key has been verified we can now sign it. We will have to import our\nprimary key to do so since we are keeping it offline for safety.</p>\n<pre><code class=\"language-bash\">gpg --import backup.gpg\n</code></pre>\n<p>List your keys to get the arch keyID:</p>\n<pre><code class=\"language-bash\">gpg --list-keys\n# ... snip ...\npub   ed25519/0x76A5EF9054449A5C 2022-10-31 [SC] [expires: 2037-10-27]\n      Key fingerprint = 3E80 CA1A 8B89 F69C BA57  D98A 76A5 EF90 5444 9A5C\nuid                   [  full  ] Pierre Schmitz &lt;pierre@archlinux.org&gt;\nuid                   [  full  ] Pierre Schmitz &lt;pierre@archlinux.de&gt;\nsub   ed25519/0xD6D13C45BFCFBAFD 2022-10-31 [A] [expires: 2037-10-27]\nsub   cv25519/0x7F56ADE50CA3D899 2022-10-31 [E] [expires: 2037-10-27]\n</code></pre>\n<p>Sign the key:</p>\n<pre><code class=\"language-bash\">gpg --sign-key 0x76A5EF9054449A5C\n</code></pre>\n<p>Now you can Export and publish the new public key and send it to a keyserver:</p>\n<pre><code class=\"language-bash\">gpg --export --armor 0x76A5EF9054449A5C &gt; archlinux-signed.asc\ngpg --send-keys 0x76A5EF9054449A5C\n</code></pre>\n<p>The more people that verify, sign, and re-export and publish their keys the\nbetter for the web of trust that gpg uses making the network more secure for\neveryone.</p>\n<h3>Edit your trust level of the key</h3>\n<pre><code class=\"language-bash\">gpg --edit-key pierre@archlinux.org\ngpg&gt; trust\nPlease decide how far you trust this user to correctly verify other users' keys\n(by looking at passports, checking fingerprints from different sources, etc.)\n\n  1 = I don't know or won't say\n  2 = I do NOT trust\n  3 = I trust marginally\n  4 = I trust fully\n  5 = I trust ultimately\n  m = back to the main menu\n\nYour decision? 3\n# Output:\npub  ed25519/0x76A5EF9054449A5C\n     created: 2022-10-31  expires: 2037-10-27  usage: SC\n     trust: marginal      validity: full\n</code></pre>\n<p>You can see that the trust is <code>marginal</code> and validity is <code>full</code>.</p>\n</details>\n",
      "date_published": "2026-01-15T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nixos_containers.html",
      "url": "https://saylesss88.github.io/nixos_containers.html",
      "title": "NixOS Containers",
      "content_html": "<h1>NixOS Containers</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/boxes.cleaned.png\" alt=\"boxes\" /></p>\n<p>NixOS containers are lightweight <code>systemd-nspawn</code> containers managed\ndeclaratively through your NixOS configuration. They allow you to run separate,\nminimal NixOS instances on the same machine, each with its own services,\npackages, and (optionally) network stack.</p>\n<ul>\n<li><a href=\"https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html?__goaway_challenge=meta-refresh&amp;__goaway_id=5497ebb54af7da76c7cff2e5210fe9ab&amp;__goaway_referer=https%3A%2F%2Fsearch.brave.com%2F\">freedesktop systemd-nspawn</a></li>\n</ul>\n<blockquote>\n<p>❗ NixOS’ containers do not provide full security out of the box (just like\ndocker). They do give you a separate chroot, but a privileged user (root) in a\ncontainer can escape the container and become root on the host system.\n–<a href=\"https://blog.beardhatcode.be/2020/12/Declarative-Nixos-Containers.html\">beardhatcode Declarative-Nixos-Containers</a></p>\n</blockquote>\n<p><strong>Common Use Cases</strong></p>\n<ul>\n<li>\n<p><strong>Isolating services</strong>: Run a web server, database, or any service in its own\ncontainer, so it can’t interfere with the main system or other services</p>\n</li>\n<li>\n<p><strong>Testing and development</strong>: Try out new configurations, packages, or services\nin a sandboxed environment.</p>\n</li>\n<li>\n<p><strong>Reproducible deployments</strong>: Because containers are defined declaratively,\nyou can reproduce the exact same environment anywhere.</p>\n</li>\n<li>\n<p><strong>Running multiple versions of a service</strong>: For example, testing different\nversions of Git or HTTP servers side by side.</p>\n</li>\n</ul>\n<hr />\n<h2>Hosting mdBook</h2>\n<p>Let’s say you want to host your mdBook. You can define a NixOS container that\nruns only the necessary service, isolated from your main system:</p>\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  ...\n}: {\n  containers.mdbook-host = {\n    autoStart = true;\n    ephemeral = true;\n    privateNetwork = false;  # Use the hosts network\n\n    bindMounts.\"/var/www/mdbook\" = {\n      hostPath = \"/home/jr/nix-book/book\";\n      isReadOnly = true;\n    };\n\n    config = {containerPkgs, ...}: {\n      networking.useDHCP = lib.mkDefault true;\n\n      services.httpd = {\n        enable = true;\n        adminAddr = \"yourEmail.com\";\n        virtualHosts.\"localhost\" = {\n          documentRoot = \"/var/www/mdbook\";\n          serverAliases = [];\n        };\n      };\n\n      networking.firewall.allowedTCPPorts = [80];\n      environment.systemPackages = with containerPkgs; [];\n      system.stateVersion = \"25.05\";\n    };\n  };\n}\n</code></pre>\n<ul>\n<li>\n<p><code>ephemeral</code>: if true, the container resets on each restart.</p>\n</li>\n<li>\n<p><code>autoStart</code>: Ensures the container starts automatically at boot.</p>\n</li>\n<li>\n<p><code>config</code>: Defines the containers NixOS configuration, just like a regular\nNixOS system.</p>\n</li>\n</ul>\n<p><strong>Mounts</strong></p>\n<pre><code class=\"language-nix\">    bindMounts.\"/var/www/mdbook\" = {\n      hostPath = \"/home/jr/nix-book/book\";\n      isReadOnly = true;\n    };\n</code></pre>\n<p>The <code>bindMount</code> settings above specify that <code>/var/www/mdbook</code> in the container\nshould be linked to <code>/home/jr/nix-book/book</code> on the host.</p>\n<p><code>hostPath</code> must exist, and <code>/var/www/mdbook</code> must not exist for this to work.</p>\n<p>The above container is fairly simple because its <code>ReadOnly</code>, things get more\ncomplicated when you need HTTPD to have write privileges.</p>\n<p>When you create and run a NixOS container like <code>mdbook-host</code>. NixOS stores the\ncontainer’s root filesystem and related container state data under:</p>\n<pre><code class=\"language-bash\">ls /var/lib/nixos-containers/\n╭────────────╮\n│ empty list │  # It's empty because we set ephemeral to true\n╰────────────╯\n</code></pre>\n<p>This directory holds the container’s own filesystem image, including system\nfiles, installed packages, configuration, and any data internal to the\ncontainer.</p>\n<hr />\n<h2>Check Container Status</h2>\n<pre><code class=\"language-bash\">nixos-container list\nmdbook-host\n</code></pre>\n<pre><code class=\"language-bash\">sudo systemctl status container@mdbook-host\n Main PID: 32938 (systemd-nspawn)\n     Status: \"Container running: Ready.\"\n</code></pre>\n<p><strong>Test HTTP server inside the container</strong></p>\n<p>We configured Apache (<code>httpd</code>) to serve <code>/var/www/mdbook</code> at <code>localhost</code></p>\n<p>Let’s check if Apache is running:</p>\n<pre><code class=\"language-bash\">sudo nixos-container run mdbook-host -- systemctl status httpd\n● httpd.service - Apache HTTPD\n     Loaded: loaded (/etc/systemd/system/httpd.service; enabled; preset: ignored)\n     Active: active (running) since Fri 2025-08-15 10:14:39 EDT; 2min 18s ago\n</code></pre>\n<p>Check the Bind Mount:</p>\n<pre><code class=\"language-bash\">sudo nixos-container run mdbook-host -- ls -l /var/www/mdbook\n</code></pre>\n<ul>\n<li>You should see an <code>index.html</code> and any other files from <code>~/nix-book/book</code></li>\n</ul>\n<p>Test the Web Server:</p>\n<pre><code class=\"language-bash\">curl http://localhost\n</code></pre>\n<ul>\n<li>You should see your book in HTTP format as raw HTML.</li>\n</ul>\n<p>Test on the web, in your browser visit:</p>\n<pre><code class=\"language-text\">http://localhost/\n</code></pre>\n<ul>\n<li>You should see your book fully served</li>\n</ul>\n<hr />\n<h3>Troubleshooting</h3>\n<p>Make sure your book has the correct permissions to allow <code>hostPath</code> to read it:</p>\n<pre><code class=\"language-bash\">sudo chmod -R o+rX ~/nix-book/book\n</code></pre>\n<p>If needed restart the container:</p>\n<pre><code class=\"language-bash\">sudo nixos-container stop mdbook-host\nsudo nixos-container start mdbook-host\n</code></pre>\n<p>Ensure that <code>/var/www/mdbook</code> is being populated:</p>\n<pre><code class=\"language-bash\">sudo nixos-container run mdbook-host -- ls -l /var/www/mdbook\n</code></pre>\n<p>You should see an <code>index.html</code> and more</p>\n<pre><code class=\"language-bash\">sudo nixos-container run mdbook-host -- systemctl status httpd\n</code></pre>\n<ul>\n<li>You should see <code>enabled</code> &amp; <code>active (running)</code></li>\n</ul>\n<p>Check the containers status:</p>\n<pre><code class=\"language-bash\">sudo nixos-container status mdbook-host\nup\n</code></pre>\n<hr />\n<h2>Why Bother Serving your book to localhost?</h2>\n<ol>\n<li>Real-time updates without rebuilding the container</li>\n</ol>\n<ul>\n<li>Files added, changed, or removed from <code>~/nix-book/book</code> on the host are\nimmediately reflected inside the container. This allows for:\n<ul>\n<li>\n<p>Rapid iteration and testing of your books content without rebuilding</p>\n</li>\n<li>\n<p>Easier debugging and fixing content or config issues on the fly.</p>\n</li>\n</ul>\n</li>\n</ul>\n<ol start=\"2\">\n<li>Keeps container images small and immutable</li>\n</ol>\n<ul>\n<li>Instead of baking book files into the container image (which requires\nrebuilding every change), the container image remains clean and generic.</li>\n</ul>\n<ol start=\"3\">\n<li>Separation of concerns</li>\n</ol>\n<ul>\n<li>The container focuses on running the service, while the content is managed\nindependently on the host. This separation improves maintainability and more.</li>\n</ul>\n<ol start=\"4\">\n<li>Data persistence</li>\n</ol>\n<ul>\n<li>Since the files live on the host, they persist independently of the containers\nlifecycle: restarting, recreating, or destroying the container won’t lose your\ncontent.</li>\n</ul>\n<ol start=\"5\">\n<li>Security Control</li>\n</ol>\n<ul>\n<li>You can carefully set permissions on the host directory, control read/write\naccess, and isolate the container runtime from sensitive data.</li>\n</ul>\n<hr />\n<h2>Removing the State</h2>\n<p>To remove <code>/var/lib/nixos-containers/mdbook-host</code>, you need to remove the\ncontainer configuration, rebuild, and then run the following commands to remove\nthe immutable sticky bits that prevent deletion.</p>\n<pre><code class=\"language-bash\"># Forcibly remove all attributes\nsudo chattr -R -i mdbook-host/\nsudo rm -rf mdbook-host/\n</code></pre>\n<h2>OCI deployment pipeline building a Rust App</h2>\n<blockquote>\n<p>If you want to use <code>mdbook-nix-repl</code> check out the README, the following shows\nhow I tested locally before eventually adding a <code>flake.nix</code> to the repo\nstreamlining this for users of the project.</p>\n</blockquote>\n<ul>\n<li><a href=\"https://github.com/saylesss88/mdbook-nix-repl\">mdbook-nix-repl README</a></li>\n</ul>\n<p>This documents how to work with a local Rust crate repository without a\n<code>flake.nix</code> for testing. The README above explains how to generate a token and\nuse the project, this is just for educational purposes if you wanted to\nimplement something similar:</p>\n<ol>\n<li><code>nix-repl-server.nix</code>, place this in the same dir as your\n<code>configuration.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  pkgs,\n  inputs,\n  ...\n}:\n\nlet\n  cfg = config.custom.nix-repl-server;\n\n  serverSource = inputs.mdbook-nix-repl + \"/server\";\n\n  # 1. Build the binary using your package definition\n  # serverPkg = pkgs.callPackage ./server-pkg.nix { };\n  serverPkg = pkgs.callPackage ./server-pkg.nix {\n    src = serverSource;\n  };\n\n  # 2. Build a minimal container image containing just the server + nix + deps\n  nixReplImage = pkgs.dockerTools.buildLayeredImage {\n    name = \"nix-repl-server\";\n    tag = \"latest\";\n\n    # dependencies needed at runtime inside the container\n    contents = [\n      serverPkg\n      pkgs.nix\n      pkgs.bashInteractive\n      pkgs.cacert\n      pkgs.tini\n      pkgs.coreutils\n    ];\n\n    config = {\n      Entrypoint = [\n        \"${pkgs.tini}/bin/tini\"\n        \"--\"\n      ];\n      Cmd = [ \"${serverPkg}/bin/nix-repl-server\" ];\n      ExposedPorts = {\n        \"8080/tcp\" = { };\n      };\n      # Important: Container must see 0.0.0.0 to receive traffic from host port mapping\n      Env = [\n        \"NIX_REPL_BIND=0.0.0.0\"\n        \"NIX_CONFIG=experimental-features = nix-command flakes\"\n        \"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt\"\n      ];\n    };\n  };\nin\n{\n  options.custom.nix-repl-server = {\n    enable = lib.mkEnableOption \"nix-repl-server container\";\n    port = lib.mkOption {\n      type = lib.types.port;\n      default = 8080;\n      description = \"Host port to map to the container\";\n    };\n    tokenFile = lib.mkOption {\n      type = lib.types.path;\n      default = \"/etc/nix-repl-server.env\";\n      description = \"Path to file containing NIX_REPL_TOKEN=...\";\n    };\n  };\n\n  config = lib.mkIf cfg.enable {\n    # Enable Podman backend\n    virtualisation.podman.enable = true;\n    virtualisation.oci-containers.backend = \"podman\";\n\n    # The OCI container definition\n    virtualisation.oci-containers.containers.nix-repl-server = {\n      image = \"nix-repl-server:latest\";\n\n      # This effectively \"loads\" the image into Podman on boot\n      imageFile = nixReplImage;\n\n      ports = [ \"127.0.0.1:${toString cfg.port}:8080\" ];\n\n      # Inject the token safely at runtime (not in Nix store)\n      environmentFiles = [ cfg.tokenFile ];\n\n      extraOptions = [\n        \"--cap-drop=ALL\"\n        \"--security-opt=no-new-privileges\"\n        \"--pull=never\" # Use the local loaded image\n      ];\n    };\n  };\n}\n</code></pre>\n<ol start=\"2\">\n<li><code>server-pkg.nix</code>, place this in the same dir as <code>nix-repl-server.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">{\n  lib,\n  rustPlatform,\n  nix,\n  pkg-config,\n  openssl,\n  makeWrapper,\n  inputs,\n  src,\n}:\n\nrustPlatform.buildRustPackage {\n  pname = \"nix-repl-server\";\n  version = \"0.1.0\";\n\n  # Point this to your actual source root (where Cargo.toml is)\n  # src = ./.;\n  inherit src;\n\n  # You must commit Cargo.lock for this to work\n  # cargoLock.lockFile = ../../../mdbook-nix-repl/server/Cargo.lock;\n  cargoLock.lockFile = \"${src}/Cargo.lock\";\n\n  postPatch = ''\n    cp Cargo.toml.inc Cargo.toml\n  '';\n\n  # Runtime dependencies (nix for evaluation)\n  nativeBuildInputs = [\n    pkg-config\n    makeWrapper\n  ];\n  buildInputs = [ openssl ];\n\n  doCheck = false;\n\n  # Ensure 'nix' is available in the path if your binary calls Command::new(\"nix\")\n  postInstall = ''\n    wrapProgram $out/bin/nix-repl-server --prefix PATH : ${lib.makeBinPath [ nix ]}\n  '';\n\n  meta = with lib; {\n    description = \"Secure Nix REPL server for mdbook-nix-repl\";\n    platforms = platforms.linux;\n  };\n}\n</code></pre>\n<ol start=\"3\">\n<li><code>flake.nix</code>, this URL leads to a Rust crate repo:</li>\n</ol>\n<pre><code class=\"language-nix\">inputs = {\n  mdbook-nix-repl = {\n    url = \"path:/home/jr/mdbook-nix-repl\";\n    flake = false;\n  };\n}\n</code></pre>\n<ol start=\"4\">\n<li><code>configuration.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">{ pkgs, inputs, ... }:\n{\n  imports = [\n    # Include the results of the hardware scan.\n    ./hardware-configuration.nix\n    ./users.nix\n    ./nix-repl-server.nix\n  ];\n\n  custom.nix-repl-server = {\n    enable = true;\n    port = 8080; # Optional, defaults to 8080\n    tokenFile = \"/etc/nix-repl-server.env\";\n  };\n# --snip--\n</code></pre>\n<hr />\n<h3>Resources</h3>\n<ul>\n<li><a href=\"https://developers.redhat.com/blog/2018/02/22/container-terminology-practical-introduction#\">RedHat A Practical Intro to Container Technology</a></li>\n</ul>\n",
      "date_published": "2026-01-11T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nixpkgs/rust_crate_to_nixpkgs.html",
      "url": "https://saylesss88.github.io/nixpkgs/rust_crate_to_nixpkgs.html",
      "title": "Packaging a Rust crate for Nixpkgs",
      "content_html": "<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<h1>Packaging a Rust crate for Nixpkgs</h1>\n<blockquote>\n<p>NOTE: This example assumes you’re packaging a crate that’s already on\ncrates.io, or you’re packaging an existing Rust project for nixpkgs.</p>\n</blockquote>\n<p>Nixpkgs is a big repository, so it helps to start with a focused workflow:\ncreate a branch, add a package under <code>pkgs/by-name/</code>, build it, then open a PR.</p>\n<hr />\n<h2>Clone nixpkgs</h2>\n<ol>\n<li>Fork and clone <code>NixOS/nixpkgs</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">git clone git@github.com:your-user/nixpkgs.git\ncd nixpkgs\ngit remote add upstream git@github.com:NixOS/nixpkgs.git\n</code></pre>\n<p>(SSH avoids HTTPS helper issues)</p>\n<ol start=\"2\">\n<li>If your clone is shallow, convert it to full history (doesn’t lose work):</li>\n</ol>\n<pre><code class=\"language-bash\">git fetch --unshallow --tags\n</code></pre>\n<hr />\n<h2>Create a branch and add package:</h2>\n<ol>\n<li>Create a branch before changes preferably:</li>\n</ol>\n<pre><code class=\"language-bash\">git switch -c mdbook-rss-feed\n</code></pre>\n<hr />\n<h2>Add the package under pkgs/by-name</h2>\n<p>New top-level packages should generally go under\n<code>pkgs/by-name/&lt;2 letters&gt;/&lt;name&gt;/package.nix</code> (e.g.\n<code>pkgs/by-name/md/mdbook-rss-feed/package.nix</code>). Packages in <code>pkgs/by-name</code> are\npicked up automatically and usually don’t require edits to <code>all-packages.nix</code>.</p>\n<hr />\n<h2>Write package.nix (Rust crate example)</h2>\n<p>Start with <code>rustPlatform.buildRustPackage</code> and <code>fetchCrate</code>:</p>\n<pre><code class=\"language-nix\">{\n  lib,\n  rustPlatform,\n  fetchCrate,\n  versionCheckHook,\n}:\nrustPlatform.buildRustPackage rec {\n  pname = \"mdbook-rss-feed\";\n  version = \"1.3.0\";\n\n  src = fetchCrate {\n    inherit pname version;\n    hash = \"output of `nix hash convert` shown below\";\n  };\n\n  cargoHash = lib.fakeHash;\n\n  nativeInstallCheckInuts = [\n    versionCheckHook\n  ];\n  doInstallCheck = true;\n\n  meta = {\n    description = \"mdBook preprocessor that generates RSS, Atom, and JSON feeds\";\n    mainProgram = \"mdbook-rss-feed\";\n    homePage = \"https://crates.io/crates/mdbook-rss-feed\";\n    license = lib.licenses.asl20;\n    maintainers = [ lib.maintainers.sayls88 ];\n  };\n}\n</code></pre>\n<hr />\n<h2>Prefetch the crate hash:</h2>\n<p>Use <code>fetchCrate</code> / <code>crate2nix</code> style workflow, or just prefetch the <code>crates.io</code>\ntarball:</p>\n<pre><code class=\"language-bash\">nix-prefetch-url \\\n  --unpack \\\n  https://crates.io/api/v1/crates/mdbook-rss-feed/1.3.0/download\n</code></pre>\n<p>That prints a base32 hash: <code>0932843lknasdlfkm2lkdnflaknldvdsvser</code></p>\n<p>Convert it to sri format:</p>\n<pre><code class=\"language-bash\">nix hash convert --hash-algo sha256 --from nix32 --to sri 0932843lknasdlfkm2lkdnflaknldvdsvser\n</code></pre>\n<p>The above commands output looks like: <code>sha256-...=</code></p>\n<p>Put the resulting <code>sha256-...</code> into <code>src.hash</code>:</p>\n<pre><code class=\"language-nix\">  src = fetchCrate {\n    inherit pname version;\n    hash = \"sha256-...\";\n  };\n</code></pre>\n<hr />\n<h2>Get cargoHash via a failing build</h2>\n<p>In the <code>nixpkgs</code> root (i.e., the <code>nixpkgs</code> directory), run:</p>\n<pre><code class=\"language-bash\">nix-build -A mdbook-rss-feed\n# OR nix3 format\nnix build .#mdbook-rss-feed\n</code></pre>\n<p>Nix will fail with a message like:</p>\n<pre><code class=\"language-text\">hash mismatch\nspecified: sha256-....\ngot: sha256-1...\n</code></pre>\n<p>Copy the <code>got</code> value into <code>cargoHash</code>, rebuild, and it should succeed.</p>\n<p>Sanity check: from <code>nixpkgs</code> root :</p>\n<pre><code class=\"language-bash\">./result/bin/mdbook-rss-feed --version\n</code></pre>\n<hr />\n<h2>Adding yourself as maintainer</h2>\n<p>Edit <code>nixpkgs/maintainers/maintainer-list.nix</code> add your user in alphabetical\norder:</p>\n<pre><code class=\"language-nix\">your-handle = {\n  email = \"you@example.com\";\n  name = \"Your Name\";\n  github = \"your-gh-handle\";\n  githubId = 12345678;\n};\n</code></pre>\n<p>If you specify <code>github</code>, nixpkgs expects <code>githubId</code> too. You can get it from:\n<code>https://api.github.com/users/&lt;user&gt;</code>.</p>\n<p>The nixpkgs maintainers prefer if you add the <code>maintainer-list.nix</code> as a\nseparate commit.</p>\n<pre><code class=\"language-bash\">git commit -m \"maintainers: add &lt;user&gt;\"\n</code></pre>\n<hr />\n<h2>Treefmt</h2>\n<p>Run treefmt the nixpkgs way, from the repo root. Run this right before you push,\nmy editors formatter does something different with single entry lists than what\nNixpkgs wants:</p>\n<pre><code class=\"language-bash\">nix develop --command treefmt\nnix fmt\n</code></pre>\n<hr />\n<h2>Rebase and push safely</h2>\n<p>From the <code>mdbook-rss-feed</code> branch:</p>\n<pre><code class=\"language-bash\">git fetch upstream --tags\ngit rebase upstream/master\n</code></pre>\n<p>Commit and push your PR branch:</p>\n<p><strong>Then commit and push</strong></p>\n<pre><code class=\"language-bash\">git commit -m \"mdbook-rss-feed: init at 1.3.0\"\n# First push\ngit push origin mdbook-rss-feed\n# Use `--force-with-lease` only if you rebased/amended and need to rewrite the PR branch.\n# git push --force-with-lease origin mdbook-rss-feed\n</code></pre>\n<p><code>--force-with-lease</code> is the recommended safe force-push for PR branches.</p>\n<p>If <code>--force-with-lease</code> says “stale info”, fetch the remote branch ref first,\nthen retry.</p>\n<p><strong>Then commit and push</strong></p>\n<pre><code class=\"language-bash\">git commit -m \"mdbook-rss-feed: init at 1.3.0\"\ngit push -u origin mdbook-rss-feed\n</code></pre>\n<p>Then:</p>\n<ol>\n<li>\n<p>Go to GitHub -&gt; your fork</p>\n</li>\n<li>\n<p>Click “Compare &amp; pull request” on the <code>mdbook-rss-feed</code> branch</p>\n</li>\n<li>\n<p>Fill out the PR template (why useful, tested on x86_64-linux, etc.)</p>\n</li>\n<li>\n<p>Submit!</p>\n</li>\n</ol>\n<p>The package will go through CI checks, and once green + approved by a\nmaintainer, it’ll land in nixpkgs.</p>\n<hr />\n<h2>Recovering from Mistakes</h2>\n<p>You’re bound to make mistakes, if you learn some Git basics it will help you\nquite a bit.</p>\n<p>You should avoid adding new commits for small fixes like typos, formatting, or\nminor adjustments requested in review. For substantial changes that add\nfunctionality, a new commit may be more appropriate.</p>\n<p>Say that we pushed our PR and one of the maintainers gave us a suggested change,\n(they want us to follow conventions and remove a trailing period from our\npackages description for this example).</p>\n<ol>\n<li>\n<p>Make the edit locally (remove the trailing period in the file)</p>\n</li>\n<li>\n<p>Stage the change: <code>git add pkgs/by-name/xx/your-package/package.nix</code> (avoid\n<code>git add -A</code> as it stages everything, which can accidentally include\nunrelated files)</p>\n</li>\n<li>\n<p>Amend the commit: <code>git commit --amend --no-edit</code>(this preserves your original\ncommit message, if you want to change the commit message use\n<code>git commit --amend</code>)</p>\n</li>\n<li>\n<p>Force push: <code>git push --force-with-lease</code> (This is safer than just using\n<code>--force</code> because it will fail if someone else has pushed commits to your\nbranch that you don’t have locally)</p>\n</li>\n</ol>\n<p><strong>Alternative for Multiple Commits</strong></p>\n<p>Interactive rebase is useful when your PR has several “WIP” commits (or you\nadded a small review fix as a separate commit) and you want to present a cleaner\nhistory before merge.</p>\n<p>You can use interactive rebase to squash all your would be small fix commits\ninto a single commit they belong to.</p>\n<p>Avoid squashing if the commits represent distinct, reviewable changes that stand\non their own.</p>\n<p><strong>Basic Workflow (squash/fixup)</strong></p>\n<ol>\n<li>Decide how many commits back you want to edit (example: last 3 commits):</li>\n</ol>\n<pre><code class=\"language-bash\">git rebase -i HEAD~3\n</code></pre>\n<ol start=\"2\">\n<li>Your editor opens with a “todo” list (oldest at top). Change later commits\nfrom <code>pick</code> to <code>fixup</code> or <code>squash</code>:</li>\n</ol>\n<ul>\n<li>\n<p><code>fixup</code> = combine into the previous commit, discard this commit message.</p>\n</li>\n<li>\n<p><code>squash</code> = combine, but keep/edit commit messages.</p>\n</li>\n</ul>\n<p>Example todo:</p>\n<pre><code class=\"language-text\">pick 1111111 mdbook-rss-feed: init at 0.1.0\npick 2222222 mdbook-rss-feed: fix trailing period\npick 3333333 mdbook-rss-feed: formatting\n</code></pre>\n<p>Change to:</p>\n<pre><code class=\"language-text\">pick 1111111 mdbook-rss-feed: init at 0.1.0\nfixup 2222222 mdbook-rss-feed: fix trailing period\nfixup 3333333 mdbook-rss-feed: formatting\n</code></pre>\n<ol start=\"3\">\n<li>Save/close: if you squashed, Git will prompt you to edit the final combined\nmessage.</li>\n</ol>\n<p><strong>Push updated history</strong></p>\n<p>You have to force-push because <code>rebase</code> rewrites commit SHAs:</p>\n<pre><code class=\"language-bash\">git push --force-with-lease\n# If something goes wrong\n# git rebase --abort\n# If you hit conflicts, fix the files, then:\n# git add &lt;files&gt;\n# git rebase --continue\n</code></pre>\n<p>If you have an unrelated change accidentally included (for example: you staged\nan extra file), it’s usually better to fix it via rebase/splitting before\nreviewers spend time re-reviewing noise.</p>\n<blockquote>\n<p>As of 01-13-26 I have been waiting for 2 weeks for the darwin checks to complete,\nI guess this <a href=\"https://discourse.nixos.org/t/ofborg-aarch64-darwin-builds-causing-bottleneck/55290\">bottleneck</a>\nhas gotten worse. I guess most PRs take about 6 weeks to resolve FYI.</p>\n</blockquote>\n",
      "date_published": "2025-12-31T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/hardening_networking.html",
      "url": "https://saylesss88.github.io/nix/hardening_networking.html",
      "title": "Hardening Networking",
      "content_html": "<h1>Hardening Networking</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<blockquote>\n<p>Since networks and systems vary, some adjustments may cause unexpected issues,\nespecially around critical components like DNS or firewalls. Always review and\ntest changes in a controlled environment before applying them broadly.</p>\n</blockquote>\n<blockquote>\n<p>Understand the trade-offs and tailor the settings to your threat model and\nworkflow. Take what’s useful, adapt as needed, and seek expert guidance for\nmore advanced scenarios.</p>\n</blockquote>\n<h2>Introduction</h2>\n<p>Every setup is unique, feel free to adapt or skip sections based on your needs.\nStart with the basics and build up as you gain confidence. The goal is\npractical, tested hardening tailored to you.</p>\n<h3>Safe Browsing / Privacy Enhancing Habits</h3>\n<p>I recently broke this chapter down and added another chapter:\n<a href=\"https://saylesss88.github.io/nix/browsing_security.html\">Browser/Browsing Security</a></p>\n<p><strong>Adopt Encrypted DNS and HTTPS Everywhere</strong></p>\n<ul>\n<li>\n<p>Configure your system and browsers to use DNS over HTTPS (DoH), DNS over TLS\n(DoT), or DNSCrypt to prevent DNS leakage. Use HTTPS-Only mode in browsers to\nencrypt all web traffic. Prefer browsers with strong privacy defaults or add\nrecommended extensions.</p>\n</li>\n<li>\n<p><a href=\"https://www.privacyguides.org/en/dns/#dnscrypt-proxy\">Privacy Guides dnscrypt-proxy recommendation</a></p>\n</li>\n<li>\n<p>Disable browser “remember password” and autofill features, clear cookies and\nsite data upon exit, and carefully vet suspicious URLs with tools like\n<a href=\"https://www.virustotal.com/gui/home/url\">VirusTotal</a>.</p>\n</li>\n</ul>\n<p><strong>Limit Account Linking and Use Unique Credentials</strong></p>\n<ul>\n<li>Create separate accounts with unique passwords instead of signing in with\nGoogle, Facebook, or similar services to limit broad data exposure from\ncompromises.</li>\n</ul>\n<p><strong>Use Metadata Cleaning Tools</strong></p>\n<ul>\n<li>\n<p>Many files like images, PDFs, and office documents contain hidden metadata\ninformation such as location data, device details, and more that can reveal\nyour identity or other sensitive information when you share files publicly.</p>\n</li>\n<li>\n<p>To protect your privacy, always sanitize files by removing this metadata\nbefore sharing. Tools like <a href=\"https://0xacab.org/jvoisin/mat2\">mat2</a> are\ndesigned to strip metadata from a wide range of media files efficiently.\n(<code>pkgs.mat2</code>). You just type <code>mat2 swappy-2025.png</code> for example and there will\nthen be a new <code>mat2 swappy-2025.cleaned.png</code> that can safely be shared.</p>\n</li>\n</ul>\n<p><strong>Use Anonymous File-Sharing Tools</strong></p>\n<ul>\n<li>For sensitive transfers, consiter tools like\n<a href=\"https://github.com/onionshare/onionshare\">OnionShare</a> that provide anonymity\nand security.(<code>pkgs.onionshare</code>)</li>\n</ul>\n<p><strong>Avoid Scanning Random QR Codes Without Verification</strong></p>\n<ul>\n<li>Use QR code scanner apps that check for malicious content before loading\nlinks.</li>\n</ul>\n<p><strong>Understand Your Threat Model</strong></p>\n<ul>\n<li>Apply these basics universally, but tailor advanced hardening according to\nyour unique environment, connectivity needs, and risk profile.</li>\n</ul>\n<p><strong>Delete cookies and site data when the browser is closed</strong>. (security not\nusability).</p>\n<p><strong>Use Strong, Unique Passwords and a Password Manager</strong></p>\n<ul>\n<li>\n<p>Avoid reused passwords by using reliable password managers like KeePassXC or\nBitwarden, both available on NixOS. Pair this with enabling two-factor\nauthentication <strong>(2FA) wherever possible</strong>.</p>\n</li>\n<li>\n<p>It’s advisable to only use the desktop version and not the browser extension\nfor a number of reasons. One is that you can store your passwords completely\noffline and have complete ownership of them.</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">environment.systemPackages = [\n    pkgs.keepassxc\n    pkgs.kpcli     # KeePass CLI\n    # OR\n    pkgs.bitwarden-desktop\n    pkgs.bitwarden-cli\n];\n</code></pre>\n<p>With KeePassXC, you can require 3 different authentication methods at the same\ntime. You can choose a password, a keyfile, and a security key where it won’t\nopen unless all 3 are present giving you additional security. All 3 might not be\nnecessary but it’s possible. It’s also easy to migrate to KeePassXC, you can\nimport your vault from many different managers.</p>\n<p>KeepassXC also makes it easy to keep your complete password database offline\nwhich can significantly reduce the risk of a breach.</p>\n<p>With Bitwarden, to enable 2 factor authentication, you need to log in with your\nmaster password through the web interface.</p>\n<ul>\n<li><a href=\"https://www.privacyguides.org/en/basics/passwords-overview/\">PrivacyGuides Intro to Passwords</a></li>\n</ul>\n<hr />\n<h3>Why Follow These Basics?</h3>\n<p>These recommended steps help protect your privacy and security while maintaining\nusability and minimizing system interruptions. They catch common threats like\nnetwork eavesdropping, password reuse, fingerprinting, and data leakage,\nproviding a solid foundation to build on.</p>\n<p>A vast majority of secure and privacy-focused browsers available for NixOS are\nbased on Firefox.</p>\n<blockquote>\n<p>❗ NOTE: Firefox does lack some security features available in Chrome and\nsandbox escapes in Linux are relatively easy. People such as madaidan say to\nnever use Linux or Firefox period when you’re worried about security and\nprivacy. I’m not personally going to jump to proprietary software with known\nbackdoors in a misguided attempt at increasing security/privacy.</p>\n</blockquote>\n<ul>\n<li><a href=\"https://techstory.in/eu-hits-google-with-3-5-billion-antitrust-fine-over-adtech-practices/\">EU Hits Google with 3.5 Billion Antitrust</a></li>\n</ul>\n<p>This <a href=\"https://grapheneos.org/usage#web-browsing\">GrapheneOS article</a>, breaks\ndown why they use Chromium-based browsers and specifically mentions that it’s\nnot recommended to use Firefox, especially on Linux because of the weak\nsandboxing.</p>\n<p>As a Chromium-based browser, Brave has been growing on me. Brave uses\nrandomization rather than standardization for fingerprinting protection. If you\nrun Cover Your Tracks with Brave, it will show a randomized fingerprint.</p>\n<details>\n<summary> ✔️ Click To Expand United States Patriot Act Overview </summary>\n<p><a href=\"https://www.csis.org/analysis/fact-sheet-section-215-usa-patriot-act\">Section 215 USA Patriot Act</a>\npermits the collection of “Tangible Things” or “Business Records”, e.g., your\nphone records, medical records, etc. for an investigation to obtain foreign\nintelligence information. If it does relate to a US person it must be relevant\nto preventing terrorism or espionage, and not be based solely on activities\nprotected by the first amendment. “Relevant” is the key word here and it is at\nthe governments discretion meaning they sweep everything and sift it later.\nCriticized for violating American citizens Fourth Amendment protections against\nwarrantless search and seizure and proven to be ineffective.</p>\n</details>\n<p>What is “normal” and allowed today might be suppressed tomorrow, look at the UK\n<a href=\"https://en.wikipedia.org/wiki/Online_Safety_Act_2023\">Online Safety Act</a>\npurported to protect children, accused of banning privacy. This is because the\nonly way to verify age is to make everyone submit KYC with their drivers license\nor ID, completely taking away any anonymity of adults and children alike.</p>\n<p>Also see\n<a href=\"https://www.bbc.com/news/articles/cq68j5g2nr1o\">BBC 4chan refuses to pay fine</a></p>\n<p>The mere existence of a surveillance state breeds fear and conformity and\nstifles free\nexpression.–<a href=\"https://theintercept.com/2016/04/28/new-study-shows-mass-surveillance-breeds-meekness-fear-and-self-censorship/\">The Intercept</a></p>\n<p>There are much more scary examples in\n<a href=\"https://thenewoil.org/en/guides/prologue/why/\">Privacy, The new Oil</a></p>\n<h2>Protections from Surveillance in the U.S.</h2>\n<details>\n<summary> ✔️ Click to Expand U.S. Surveillance protections </summary>\n<blockquote>\n<p>⚠️ A crucial caveat to keep in mind regarding surveillance protections in the\nU.S., whether grounded in the Fourth Amendment, the First Amendment, or\nstatutory laws is that <strong>these protections are not foolproof and have\nrepeatedly failed or been circumvented in practice</strong>.</p>\n</blockquote>\n<ul>\n<li>\n<p><strong>Fourth Amendment Basics</strong>: It demands reasonableness in searches and usually\nrequires a warrant. This means government agents cannot arbitrarily listen to\nyour private communications or search your digital data without judicial\napproval</p>\n</li>\n<li>\n<p><strong>Electronic Surveillance Challenges</strong>: Courts have wrestled with how the\nFourth Amendment applies to modern communications. The Supreme Court has ruled\nin some cases that pervasive or non-consensual electronic surveillance\nviolates reasonable expectations of privacy, but other rulings have allowed\nbroader state actions in national security contexts.</p>\n</li>\n<li>\n<p><strong>The Third-Party Doctrine</strong>: A major limitation arises from the “third-party\ndoctrine,” which holds that information voluntarily shared with third parties\n(like phone companies or internet providers) has reduced Fourth Amendment\nprotections. This means data held by third parties may be subject to\ngovernment access without a warrant in some cases</p>\n</li>\n<li>\n<p><strong>The First Amendment</strong> guarantees free speech and the freedom to receive\ninformation without government censorship or intimidation. Excessive or\nsecretive government surveillance can chill free speech by making people\nafraid their communications are monitored, discouraging open expression and\nparticipation in public discourse.</p>\n<ul>\n<li>Advocates argue that courts should recognize government surveillance not\nonly as a Fourth Amendment search issue but also as a First Amendment\nviolation where surveillance suppresses or chills constitutionally protected\nexpression.</li>\n</ul>\n</li>\n</ul>\n<p>While the Fourth Amendment traditionally governs searches and surveillance\nlegality, the First Amendment frames the broader impact on free speech and\ndemocratic engagement. Invoking both provides a more comprehensive\nconstitutional shield against intrusive surveillance practices.</p>\n</details>\n<hr />\n<h2>Encrypted DNS</h2>\n<p>DNS (Domain Name System) resolution is the process of translating a website’s\ndomain name into its corresponding IP address. By default, this traffic isn’t\nencrypted, which means anyone on the network, from your ISP to potential\nhackers, can see the websites you’re trying to visit. <strong>Encrypted DNS</strong> uses\nprotocols to scramble this information, protecting your queries and responses\nfrom being intercepted and viewed by others.</p>\n<blockquote>\n<p>❗ NOTE: There are many other ways for someone monitoring your traffic to see\nwhat domain you looked up via DNS that it’s effectiveness is questionable\nwithout also using Tor or a VPN. Encrypted DNS will not help you hide any of\nyour browsing activity.</p>\n</blockquote>\n<p>There are 3 main types of DNS protection:</p>\n<ul>\n<li>\n<p><strong>DNS over HTTPS (DoH)</strong>: Uses the HTTPS protocol to encrypt data between the\nclient and the resolver.</p>\n</li>\n<li>\n<p><strong>DNS over TLS (DoT)</strong>: Similar to (DoH), differs in the methods used for\nencryption and delivery using a separate port from HTTPS.</p>\n</li>\n<li>\n<p><strong>DNSCrypt</strong>: Uses end-to-end encryption with the added benefit of being able\nto prevent DNS spoofing attacks.</p>\n</li>\n</ul>\n<p>Useful resources:</p>\n<details>\n<summary> ✔️ Click to Expand DNS Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Encrypted_DNS\">NixOS Wiki Encrypted DNS</a></p>\n</li>\n<li>\n<p><a href=\"https://www.cloudflare.com/learning/dns/what-is-dns/\">Domain Name System (DNS)</a></p>\n</li>\n<li>\n<p><a href=\"https://en.wikipedia.org/wiki/DNS_over_HTTPS\">Wikipedia DNS over HTTPS (DoH)</a></p>\n</li>\n<li>\n<p><a href=\"https://en.wikipedia.org/wiki/DNS_over_TLS\">Wikipedia DNS over TLS (DoT)</a></p>\n</li>\n<li>\n<p><a href=\"https://blog.cloudflare.com/dns-encryption-explained/\">Cloudflare Dns Encryption Explained</a></p>\n</li>\n<li>\n<p><a href=\"https://nordvpn.com/blog/encrypted-dns-traffic/\">NordVPN Encrypted Dns Traffic</a></p>\n</li>\n</ul>\n<p><strong>Hot Take</strong>:</p>\n<ul>\n<li><a href=\"https://madaidans-insecurities.github.io/encrypted-dns.html\">Encrypted DNS is ineffective without a VPN or Tor by madaidan</a></li>\n</ul>\n</details>\n<p>The following sets up dnscrypt-proxy using ODoH (Oblivious DNS over HTTPS) with\nan oisd blocklist:</p>\n<p>Add <code>oisd</code> to your flake inputs:</p>\n<pre><code class=\"language-nix\"># flake.nix\ninputs = {\n    oisd = {\n      url = \"https://big.oisd.nl/domainswild\";\n      flake = false;\n    };\n};\n</code></pre>\n<details>\n<summary> ✔️ Add more blocklists: HaGeZi Multi PRO </summary>\n<p>To use the Hagezi Multi PRO Blocklist either with oisd or alone you could do the\nfollowing:</p>\n<pre><code class=\"language-nix\"># flake.nix\ninputs = {\n    oisd = {\n      url = \"https://big.oisd.nl/domainswild\";\n      flake = false;\n    };\n    hagezi = {\n      url = \"https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/pro-onlydomains.txt\";\n      flake = false;\n    };\n};\n</code></pre>\n<p>add it to the <code>extraBlocklist</code> variable in the following <code>dnscrypt-proxy.nix</code>:</p>\n<pre><code class=\"language-nix\"># dnscrypt-proxy.nix\nextraBlocklist = builtins.readFile inputs.hagezi;\n</code></pre>\n<p>More blocklist url’s:</p>\n<pre><code class=\"language-text\"># NextDNS CNAME cloaking list\nhttps://raw.githubusercontent.com/nextdns/cname-cloaking-blocklist/master/domains\n\n# AdGuard Simplified Domain Names filter\nhttps://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt\n\n# OISD Big list\nhttps://big.oisd.nl/domainswild\n\n# HaGeZi Multi PRO\nhttps://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/pro-onlydomains.txt\n\n# HaGeZi Threat Intelligence Feeds\nhttps://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/tif-onlydomains.txt\n</code></pre>\n</details>\n<blockquote>\n<p>❗ NOTE: The <code>oisd</code> blocklist is a plain text file that updates frequently.\nThis can cause <code>nh os switch</code> to fail with a <code>NarHash</code> mismatch error. To fix\nthis, you need to run <code>nix flake update</code> to refresh the blocklist and its hash\nin your <code>flake.lock</code> file. After that, you can run your <code>nh</code> command again.</p>\n</blockquote>\n<p>And the import the following into your <code>configuration.nix</code>:</p>\n<pre><code class=\"language-nix\"># dnscrypt-proxy.nix\n{\n  pkgs,\n  lib,\n  inputs,\n  ...\n}: let\n  blocklist_base = builtins.readFile inputs.oisd;\n  extraBlocklist = \"\";\n  blocklist_txt = pkgs.writeText \"blocklist.txt\" ''\n    ${extraBlocklist}\n    ${blocklist_base}\n  '';\n  hasIPv6Internet = true;\n  StateDirName = \"dnscrypt-proxy\"; # Used for systemd StateDirectory\n  StatePath = \"/var/lib/${StateDirName}\";\nin {\n  networking = {\n    nameservers = [\"127.0.0.1\" \"::1\"];\n    networkmanager.dns = \"none\";\n  };\n\n  services.resolved.enable = lib.mkForce false;\n\n  services.dnscrypt-proxy = {\n    enable = true;\n    settings = {\n      sources.public-resolvers = {\n        urls = [\n          \"https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md\"\n          \"https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md\"\n        ];\n        minisign_key = \"RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3\";\n        cache_file = \"${StatePath}/public-resolvers.md\";\n      };\n\n      sources.relays = {\n        urls = [\n          \"https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md\"\n          \"https://download.dnscrypt.info/resolvers-list/v3/relays.md\"\n        ];\n        cache_file = \"${StatePath}/relays.md\";\n        minisign_key = \"RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3\";\n      };\n\n      sources.odoh-servers = {\n        urls = [\n          \"https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md\"\n          \"https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md\"\n        ];\n        cache_file = \"${StatePath}/odoh-servers.md\";\n        minisign_key = \"RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3\";\n      };\n\n      sources.odoh-relays = {\n        urls = [\n          \"https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md\"\n          \"https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md\"\n        ];\n        cache_file = \"${StatePath}/odoh-relays.md\";\n        minisign_key = \"RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3\";\n      };\n\n      server_names = [\"odoh-cloudflare\" \"odoh-snowstorm\"];\n\n      # This creates the [anonymized_dns] section in dnscrypt-proxy.toml\n      anonymized_dns = {\n        skip_incompatible = true;\n        routes = [\n          {\n            server_name = \"odoh-snowstorm\";\n            via = [\"odohrelay-crypto-sx\"];\n          }\n          {\n            server_name = \"odoh-cloudflare\";\n            via = [\"odohrelay-crypto-sx\"];\n          }\n        ];\n      };\n\n      ipv6_servers = hasIPv6Internet;\n      block_ipv6 = !hasIPv6Internet;\n      blocked_names.blocked_names_file = \"${blocklist_txt}\";\n      require_dnssec = true;\n      require_nolog = false;\n      require_nofilter = false;\n      odoh_servers = true;\n      dnscrypt_servers = true;\n    };\n  };\n\n  # This creates /var/lib/dnscrypt-proxy with correct permissions\n  systemd.services.dnscrypt-proxy2.serviceConfig.StateDirectory = StateDirName;\n}\n</code></pre>\n<p>This module follows a “Zero Trust” model for your internet traffic, ensuring no\nsingle entity can see both <strong>who you are</strong> and <strong>where you are going</strong>.</p>\n<pre><code class=\"language-bash\"># You should see that dnscrypt-proxy chooses the Server with the lowest initial latency\nsudo systemctl status dnscrypt-proxy2\n# verify that dnscrypt-proxy is listening\nsudo ss -lnp | grep 53\n# Test a DNS query, if you get valid responses it's working\ndig @127.0.0.1 example.com +short\n# check the logs\nsudo journalctl -u dnscrypt-proxy2\n</code></pre>\n<p><code>dnscrypt-proxy2</code> acts as your local DNS resolver listening on your machine\n(<code>127.0.0.1</code>) for IPv4 and <code>::1</code> for iPv6.</p>\n<p>The system’s DNS settings (<code>networking.nameservers</code>) point to localhost, so\n<strong>all DNS queries</strong> go to dnscrypt-proxy accept for your browser. Your browser\nhas to be configured separately with a local resolver in which I haven’t figured\nout yet. I recommend setting your browsers DNS over HTTPS to strict with a\nrespected custom DNS resolver such as <code>https://dns.quad9.net/dns-query</code>.</p>\n<p><code>inputs.oisd</code> refers to the flake input oisd blocklist, it prevents your device\nfrom connecting to unwanted or harmful domains.</p>\n<p><code>dnscrypt-proxy2</code> then encrypts and forwards our DNS requests to third-party\npublic DNSCrypt or DoH servers.</p>\n<ul>\n<li>ODoH Relays: This is the “Oblivious” part. It breaks the link between your IP\naddress and your browsing history.</li>\n</ul>\n<h3>Setting up Tailscale</h3>\n<p>I was surprised at how easy this actually was to set up. Either go to\n<a href=\"https://www.tailscale.com\">https://www.tailscale.com</a> and/or download the app for either Android or IOS,\nsign up with your identity provider, and click <code>Start connecting devices -&gt;</code></p>\n<ul>\n<li><a href=\"https://tailscale.com/kb/1017/install\">Tailscale quickstart</a></li>\n</ul>\n<p>To add tailscale to NixOS:</p>\n<pre><code class=\"language-nix\"># tailscale.nix\n{...}: {\n  services.tailscale.enable = true;\n  # Tell the firewall to implicitly trust packets routed over Tailscale:\n  networking.firewall.trustedInterfaces = [\"tailscale0\"];\n}\n</code></pre>\n<p>Tailscale will automatically use the hostname of your device as the name of the\nnetwork. If you want to change it to something else:</p>\n<pre><code class=\"language-bash\">sudo tailscale set --hostname=&lt;name&gt;\n# You can also give your account a nickname\nsudo tailscale set --nickname=&lt;name&gt;\n</code></pre>\n<p>This allows you to refer to your network by <code>name</code> rather than IP address.</p>\n<p>Tailscale uses <a href=\"https://tailscale.com/kb/1081/magicdns\">MagicDNS</a> which is\nenabled by default, and they recommend you keep it enabled.</p>\n<p>The docs say that by default, devices in your tailnet prefer their local DNS\nsettings and only use the tailnet’s DNS servers when needed. I had to completely\ndisable my Androids DNS settings for tailscale to access the internet through\nMagicDNS.</p>\n<pre><code class=\"language-bash\">sudo tailscale set --accept-dns=false\n</code></pre>\n<p>To connect to tailscale after rebuilding you can run:</p>\n<pre><code class=\"language-bash\">sudo tailscale up\n</code></pre>\n<p>Use <code>nslookup</code> to review and debug DNS responses:</p>\n<pre><code class=\"language-bash\">nslookup google.com\nServer:         127.0.0.1\nAddress:        127.0.0.1#53\n\nNon-authoritative answer:\nName:   google.com\nAddress: 142.251.40.206\nName:   google.com\nAddress: 2a00:1450:4001:827::200e\n</code></pre>\n<ul>\n<li>The <code>127.0.0.1#53</code> indicate that instead of using the DNS server pushed by\nyour ISP, router, or Tailscale’s MagicDNS, the system is sending all DNS\nrequests through the loopback device to <code>dnscrypt-proxy</code> in my case.</li>\n</ul>\n<p>Get the status of your connections to other Tailscale devices:</p>\n<pre><code class=\"language-bash\">tailscale status\n1           2         3           4         5\n100.1.2.3   device-a  apenwarr@   linux     active; direct &lt;ip-port&gt;, tx 1116 rx 1124\n100.4.5.6   device-b  crawshaw@   macOS     active; relay &lt;relay-server&gt;, tx 1351 rx 4262\n100.7.8.9   device-c  danderson@  windows   idle; tx 1214 rx 50\n100.0.1.2   device-d  ross@       iOS       —\n</code></pre>\n<ul>\n<li>\n<p><a href=\"https://tailscale.com/kb/1196/security-hardening\">Tailscale Best Practices</a></p>\n</li>\n<li>\n<p><a href=\"https://tailscale.com/kb/1080/cli\">Tailscale CLI</a></p>\n</li>\n<li>\n<p>There is much more you can do with Tailscale, including integrating\nMullvad-VPN and using Exit Nodes.</p>\n</li>\n</ul>\n<h2>MAC Randomization</h2>\n<p>All network cards have a unique identifier called a MAC address. They’re stored\nin hardware and are used to assign an address to computers on the local network.</p>\n<p>The MAC address is typically only traceable on the local network, it’s not\npassively sent out beyond the local router making it more critical on untrusted,\npublic networks.</p>\n<p>Leak-proof MAC randomization is very difficult to implement:</p>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/Dev/MAC#Leak-proof_MAC_Randomization_-_Technical_Implementation_Challenges\">Leak-proof MAC Randomization Implementation Challenges</a></li>\n</ul>\n<p>Android and iPhone already implement MAC Randomization by default.</p>\n<p>MAC Randomization enhances privacy by making it harder for third parties to\ntrack users across different networks.</p>\n<p>Randomizing MAC adresses obscures a device’s unique hardware identity when\nscanning for or connecting to Wi-Fi, blocking passive tracking as well as\nlocation tracking across networks.</p>\n<p>If you use NetworkManager you can set MAC randomization with:</p>\n<pre><code class=\"language-nix\">    networking = {\n      networkmanager = {\n        enable = true;\n        wifi.scanRandMacAddress = true;\n        wifi.macAddress = \"random\";\n        plugins = [];\n      };\n</code></pre>\n<p>Right when I rebuilt, I got an alert from my router saying that a new device\njust connected to the network.</p>\n<p>There is also a utility for viewing/manipulating the MAC address of network\ninterfaces, <code>pkgs.macchanger</code>. This is less reliable than the NetworkManager\nsetting.</p>\n<h2>Firewalls</h2>\n<p>NixOS includes an integrated firewall based on iptables/nftables.</p>\n<details>\n<summary> ✔️ Click to Expand Firewall Resources </summary>\n<p><a href=\"https://www.cloudflare.com/learning/security/what-is-a-firewall/\">Cloudflare What is a Firewall</a></p>\n<p><a href=\"https://linux-audit.com/networking/nftables/nftables-beginners-guide-to-traffic-filtering/\">Beginners guide to nftables</a></p>\n<p><a href=\"https://wiki.archlinux.org/title/Nftables\">Arch Wiki nftables</a></p>\n</details>\n<p>The following firewall setup is based on the dnscrypt setup above utilizing\nnftables.</p>\n<p>This nftables firewall configuration is a strong recommended practice for\nenforcing encrypted DNS on your system by restricting all outbound DNS traffic\nto a local dnscrypt-proxy process. It greatly reduces DNS leak risks and\nenforces privacy by limiting DNS queries to trusted, encrypted upstream\nservers.(This was edited on 08-08-25) replace <code>&lt;DNSCRYPT-UID&gt;</code> with the UID\ngiven from the command <code>ps -o uid,user,pid,cmd -C dnscrypt-proxy</code>:</p>\n<pre><code class=\"language-nix\">{ ... }: {\n  networking.nftables = {\n    enable = true;\n\n    ruleset = ''\n      table inet filter {\n        chain output {\n          type filter hook output priority 0; policy accept;\n\n          # Allow localhost DNS for dnscrypt-proxy2\n          ip daddr 127.0.0.1 udp dport 53 accept\n          ip6 daddr ::1 udp dport 53 accept\n          ip daddr 127.0.0.1 tcp dport 53 accept\n          ip6 daddr ::1 tcp dport 53 accept\n\n          # Allow dnscrypt-proxy2 to talk to upstream servers\n          # Replace &lt;DNSCRYPT-UID&gt; with:\n          # ps -o uid,user,pid,cmd -C dnscrypt-proxy\n          meta skuid &lt;DNSCRYPT-UID&gt; udp dport { 443, 853 } accept\n          meta skuid &lt;DNSCRYPT-UID&gt; tcp dport { 443, 853 } accept\n\n          # Block all other outbound DNS\n          udp dport { 53, 853 } drop\n          tcp dport { 53, 853 } drop\n        }\n      }\n    '';\n  };\n  networking.firewall = {\n    enable = true;\n    allowedTCPPorts = [\n      # Ports open for inbound connections.\n      # Limit these to reduce the attack surface.\n\n      22 # SSH – Keep open only if you need remote access.\n         # To change the SSH port in NixOS:\n         # services.openssh.ports = [ 2222 ];\n         # Update this list to match the new port.\n\n      # 53  # DNS – Only if running a public DNS server.\n      # 80  # HTTP – Only if hosting a website.\n      # 443 # HTTPS – Only if hosting a secure website.\n    ];\n    allowedUDPPorts = [\n      # Ports open for inbound UDP traffic.\n      # Most NixOS workstations won't need any here.\n\n      # 53 # DNS – Only if running a public DNS server.\n    ];\n  };\n}\n</code></pre>\n<details>\n<summary> ✔️ Click to Expand Tip on changing the default SSH Port </summary>\n<blockquote>\n<p>❗ TIP: Reduce SSH noise by changing the default port On most systems, SSH\nlistens on TCP port 22 — which means automated bots and scanners will hit it\nconstantly. While this doesn’t replace real security measures, moving SSH to a\ndifferent port drastically cuts down on drive-by brute-force attempts you’ll\nsee in your logs.</p>\n<p>In NixOS, change both the SSH daemon port and your firewall rule:</p>\n<pre><code class=\"language-nix\"> # Example: Move SSH to port 2222\n networking.firewall.allowedTCPPorts = [ 2222 ];\n services.openssh.ports = [ 2222 ];\n</code></pre>\n<ul>\n<li>After rebuilding, test from another terminal/session before closing your\nexisting one:</li>\n</ul>\n<pre><code class=\"language-bash\">ssh -p 2222 user@host\n</code></pre>\n</blockquote>\n</details>\n<p><code>nft</code> is a cli tool used to set up, maintain and inspect packet filtering and\nclassification rules in the Linux kernel, in the nftables framework. The Linux\nkernel subsystem is known as nftables, and ‘nf’ stands for Netfilter.–<code>man nft</code></p>\n<pre><code class=\"language-bash\">sudo nft list ruleset\n</code></pre>\n<ul>\n<li>Since we declare our firewall, we’ll only use <code>nft</code> to inspect our ruleset.</li>\n</ul>\n<h2>NixOS Firewall vs <code>nftables</code> Ruleset</h2>\n<p><code>networking.nftables</code>: This section provides a raw <code>nftables</code> ruleset that gives\nyou granular, low-level control. The rules here are more specific and are meant\nto handle the intricate logic of the DNS proxy setup. They will be applied\ndirectly to the kernel’s <code>nftables</code> subsystem and prevent DNS leaks.</p>\n<p><code>networking.firewall</code>: This is a higher-level, simpler NixOS option that uses\n<code>iptables</code> rules to open ports for inbound traffic. The rules defined here\n(allowing port 22) is for incoming SSH connections to the machine, not for\noutbound traffic, so they do not interfere with the <code>nftables</code> rules that filter\nthe outgoing traffic. (Make sure to comment out or remove this if you don’t SSH\ninto your machine).</p>\n<p>The firewall ensures only authorized, local encrypted DNS proxy process can\nspeak DNS with the outside world, and that all other DNS requests from any other\nprocess are blocked unless they’re to <code>127.0.0.1</code> (our local proxy). This is a\nrobust policy against both DNS leaks and local compromise.</p>\n<h2>Testing</h2>\n<p>Review listening ports: After each rebuild, use <code>ss -tlpn</code>, <code>nmap</code> or <code>netstat</code>\nto see which services are accepting connections. Close or firewall anything\nunnecessary.</p>\n<p>You can also test firewall DNS restrictions using <code>dig</code>:</p>\n<pre><code class=\"language-bash\">dig @127.0.0.1 example.com  # Should work\n\ndig @8.8.8.8 example.com    # Should fail/time out for normal users\n</code></pre>\n<ul>\n<li>This test is actually what alerted me of an improper configuration in the\nabove firewalls nftables rules allowing me to fix it. Initially the second\n<code>dig</code> command gave results letting me know that the restrictions weren’t being\napplied correctly.</li>\n</ul>\n<p>Since we defined an <code>output</code> chain inside <code>table inet filter</code> with the line:</p>\n<pre><code class=\"language-bash\">type filter hook output priority 0; policy accept;\n</code></pre>\n<p>This attaches the chain to the kernel’s OUTPUT hook, so all locally generated\npackets, including DNS queries are filtered by this chain.</p>\n<p>Within this chain, the rules:</p>\n<ul>\n<li>\n<p>Explicitly allow DNS queries to localhost addresses (<code>127.0.0.1</code> and <code>::1</code>).</p>\n</li>\n<li>\n<p>Allow the <code>dnscrypt-proxy</code> process (running with UID <code>62396</code>) to send DNS\nqueries on ports 443 and 853 (for DNS-over-HTTPS and DNS-over-TLS).</p>\n</li>\n<li>\n<p>Drop all other outbound DNS traffic on ports <code>53</code> and <code>853</code>.</p>\n</li>\n</ul>\n<p>Because of this setup, dig queries to your local resolver at <code>127.0.0.1</code> pass,\nbut queries directly to public DNS servers like <code>8.8.8.8</code> are blocked for\nusers/processes other than the allowed DNS proxy.</p>\n<h2>OpenSnitch</h2>\n<ul>\n<li><a href=\"https://wiki.nixos.org/wiki/OpenSnitch\">NixOS Wiki OpenSnitch</a></li>\n</ul>\n<p><a href=\"https://github.com/evilsocket/opensnitch\">Opensnitch</a> is an open-source\napplication firewall that focuses on monitoring and controlling outgoing network\nconnections on a per-application basis.</p>\n<p>This can be used to block apps from accessing the internet that shouldn’t need\nto (i.e., block telemetry and more). Opensnitch will report that the app has\nattempted to make an outbound internet connection and block it or allow it based\non the rules you set.</p>\n<h3>Resources</h3>\n<details>\n<summary> ✔️ Click to Expand Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://cloudflare.com/learning/ssl/what-is-https\">Cloudflare What is HTTPS</a></p>\n</li>\n<li>\n<p><a href=\"https://ssd.eff.org/\">Surveillance Self-Defence</a> has a lot of helpful info to\nprotect your privacy.</p>\n</li>\n<li>\n<p><a href=\"https://ssd.eff.org/module/what-fingerprinting\">What is Fingerprinting</a>, more\nthan you realize is being tracked constantly.</p>\n</li>\n<li>\n<p><a href=\"https://oisd.nl/\">oisd.nl</a> the oisd website</p>\n</li>\n<li>\n<p>For potentially dangerous file types like PDFs, office documents, or images,\nespecially those downloaded from untrusted sources such as torrents, consider\nconverting them to a safe PDF format with\n<a href=\"https://github.com/freedomofpress/dangerzone\">dangerzone</a>. Dangerzone not\nonly removes metadata but also applies robust sanitization to neutralize\nmalicious content.</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Librewolf\">NixOS Wiki LibreWolf</a>, the options in\nthe wiki make it less secure and aren’t recommended settings to use. They\nexplicitly disable several of LibreWolf’s default privacy-enhancing features,\nsuch as fingerprinting resistance and clearing session data on shutdown.</p>\n</li>\n<li>\n<p><a href=\"https://librewolf.net/docs/features/\">LibreWolf Features</a> You still need to\nenable DNS over HTTPS through privacy settings.</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/SearXNG\">SearXNG on NixOS</a></p>\n<ul>\n<li><a href=\"https://docs.searxng.org/\">Welcome to SearXNG</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"https://brainfucksec.github.io/firefox-hardening-guide\">Firefox Hardening Guide</a></p>\n</li>\n<li>\n<p><a href=\"https://www.ghacks.net/2015/08/18/a-comprehensive-list-of-firefox-privacy-and-security-settings/\">Firefox ghacks</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/arkenfox/user.js\">Arkenfox</a></p>\n</li>\n<li>\n<p><a href=\"https://www.privacytools.io/private-browser\">PrivacyTools.io</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/simeononsecurity/FireFox-Privacy-Script\">simeononsecurity Firefox-Privacy-Script</a></p>\n</li>\n<li>\n<p><a href=\"https://brainfucksec.github.io/firefox-hardening-guide\">brianfucksec firefox-hardening-Guide 2023</a></p>\n</li>\n<li>\n<p><a href=\"https://simeononsecurity.com/guides/enhance-firefox-security-configuring-guide/\">STIG Firefox Hardening</a></p>\n</li>\n</ul>\n<blockquote>\n<p>If you should trust the U.S. Governments recommendations is another story but\nit can be good to compare and contrast with other trusted resources. You’ll\nhave to think whether the CISA recommending that everyone uses Signal is solid\nadvice or guiding you towards a honeypot, I can’t say for sure.</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://stigviewer.com/stigs/mozilla_firefox\">Mozilla Firefox Security Technical Implementation Guide</a>\nThe STIG for Mozilla Firefox (Security Technical Implementation Guide) is a\nset of security configuration standards developed by the U.S. Department of\nDefense. They are created by the Defense Information Systems Agency (DISA) to\nsecure and harden DoD information systems and software.</p>\n</li>\n<li>\n<p><a href=\"https://thenewoil.org/en/guides/prologue/why/\">Privacy, The New Oil (Why Privacy &amp; Security Matter)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.privacyguides.org/en/\">PrivacyGuides</a></p>\n</li>\n<li>\n<p><a href=\"https://relay.firefox.com/accounts/profile/\">Firefox Relay</a> can be used to\ncreate email aliases that forward to your real email address. The paid plan\nalso lets you create phone number aliases that forward to your phone number.</p>\n</li>\n<li>\n<p><a href=\"https://zebracrossing.narwhalacademy.org/\">Zebra Crossing digital safety checklist</a></p>\n</li>\n<li>\n<p><a href=\"https://datadetoxkit.org/en/privacy/essentials#step-1\">DataDetoxKit</a></p>\n</li>\n<li>\n<p><a href=\"https://datadetoxkit.org/en/privacy/degooglise/\">DataDetox Degooglise</a></p>\n</li>\n<li>\n<p><a href=\"https://tb-manual.torproject.org/\">Tor Browser User Manual</a></p>\n</li>\n<li>\n<p><a href=\"https://gitlab.torproject.org/tpo/team/-/wikis/home\">Tor Wiki</a></p>\n</li>\n<li>\n<p><a href=\"https://tldp.org/LDP/nag2/x-087-2-intro.html\">Linux Network Administrators Guide</a></p>\n</li>\n<li>\n<p><a href=\"https://www.ibm.com/think/topics/networking\">IBM Networking</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2025-12-25T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/browsing_security.html",
      "url": "https://saylesss88.github.io/nix/browsing_security.html",
      "title": "Browsing Security",
      "content_html": "<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<h1>Browser/Browsing Security: Defense in Depth</h1>\n<blockquote>\n<p>“The major problem with current systems is their inability to provide\neffective isolation between various programs running on one machine. E.g. if\nthe user’s Web browser gets compromised (due to a bug exploited by a malicious\nweb site), the OS is usually unable to protect other user’s applications and\ndata from also being compromised.” –Qubes arch-spec</p>\n</blockquote>\n<p>The web browser is the most complex, most exposed, and most vulnerable\napplication on a hardened Linux system. It is your primary interface with the\ninternet and, consequently, the primary vector for exploitation and tracking.</p>\n<p>The <strong>Three Pillars of Web Defense</strong> To secure your browsing, you must balance\nthree often-conflicting goals:</p>\n<ol>\n<li>\n<p><strong>Security (Exploit Mitigation)</strong>: Preventing malicious sites from escaping\nthe browser sandbox to access your local files or execute code.</p>\n</li>\n<li>\n<p><strong>Privacy (Tracking Protection)</strong>: Preventing advertisers and sites from\nlinking your current session to your real-world identity or browsing history.</p>\n</li>\n<li>\n<p><strong>Anonymity (Identity Obfuscation)</strong>: Making your traffic indistinguishable\nfrom thousands of other users to hide your physical location and legal\nidentity.</p>\n</li>\n</ol>\n<h2>Methods of Protection</h2>\n<ul>\n<li>\n<p><strong>Browser hardening</strong> focuses on reducing attack surface and blocking tracking\nby disabling or restricting features like JavaScript, cookies, telemetry, and\nthird-party scripts.</p>\n</li>\n<li>\n<p><strong>Fingerprint protection</strong>, on the other hand, aims to make your browser\nindistinguishable from others. Instead of just blocking data collection, it\nensures that your browser’s configuration; screen size, fonts, user agent,\netc. matches a large group of users, so you blend in.</p>\n</li>\n<li>\n<p><strong>Anonymity</strong>: Maximizing anonymity often means restricting or masking\nfeatures (setting a generic fingerprint, disabling browser APIs, blocking\ntrackers) so the browser blends in with many others. This reduces uniqueness\nbut can break website functionality, cause CAPTCHAs, and limit usability.</p>\n</li>\n<li>\n<p><strong>Browser compartmentalization</strong> is a technique where different browsers are\ndedicated to distinct online activities to isolate cookies, trackers, and\nbrowsing data. For example, Mullvad Browser can be used solely for activities\nwhere fingerprinting resistance is critical, such as anonymous browsing or\nvisiting privacy-sensitive sites. Meanwhile, a hardened LibreWolf or Firefox\ncan be used for general browsing, email, or banking where you want solid\nsecurity and feature flexibility but aren’t as concerned about fingerprint\nuniqueness.</p>\n</li>\n</ul>\n<p>On a hardened Linux system, the browser is most often the weakest link exposed\nto the internet, and so security, privacy, and anti-tracking features of\nbrowsers are now as important, or even more important than platform-level\nprotections.</p>\n<hr />\n<p>Browsers leak identity in two main ways: <strong>network identifiers</strong> (IP address,\nDNS, TLS metadata) and <strong>browser identifiers</strong> (fingerprinting + tracking). This\nchapter focuses on the browser side first, then covers when a VPN or Tor changes\nthe network side. Before tweaking anything, pick the browsing goal (anonymity vs\nprivacy vs convenience), because the “best” settings differ.</p>\n<p>If the goal is blending in, prefer a browser that ships with a shared,\nconsistent fingerprint (Tor Browser / Mullvad Browser). If the goal is mainly\nreducing cross-site tracking for normal browsing, a hardened Firefox/LibreWolf\nprofile with minimal extensions is usually easier to live with.</p>\n<h3>Fingerprinting</h3>\n<p>Modern web APIs make rich, customized experiences possible, but they also reveal\nenough low‑level details about your device and browser to build a unique\nfingerprint. This fingerprint can be used for hidden, persistent tracking, even\nwhen cookies are blocked.</p>\n<p>Browser fingerprinting is a tracking technique, often done by third-party\ncompanies that specialize in it. They provide code (usually JavaScript) that a\nwebsite owner can embed on their site. When you visit the site, the script runs\nin the background, silently collecting data about your device and browser.</p>\n<ul>\n<li><a href=\"https://en.wikipedia.org/wiki/Entropy_(computing)\">Entropy</a>: in this\ncontext, is a measure of how much unique information a specific browser\nfeature contributes to your fingerprint. It’s often quantified in <strong>bits of\nentropy</strong>, where higher bits mean more uniqueness (i.e., easier to identify\nyou).\n<ul>\n<li>A “bit” is a basic unit of information for computers. Entropy measuring\nsites results are measured in “bits of identifying information”.</li>\n</ul>\n</li>\n</ul>\n<p>There are two main approaches to obfuscating your fingerprint:</p>\n<ul>\n<li>\n<p><strong>Standardization</strong>: Make browsers standardized and therefore have the same\nfingerprint to blend into a crowd. This is what Tor and Mullvad Browser do.\nBest for anonymity; increases the crowd you blend into, but may decrease\nusability (site breakage, CAPTCHAs); adversaries may still find subtle\ndifferences.</p>\n</li>\n<li>\n<p><strong>Randomization</strong>: Randomize fingerprint metrics so it’s not directly linkable\nto you. Brave has this feature, if you run coveryourtracks with Brave you will\nget a result of “your browser has a randomized fingerprint”. This is good for\nprivacy but may be detectable by advanced scripts.</p>\n</li>\n</ul>\n<h3>Fingerprint Testing</h3>\n<p>You can test your browser to see how well you are protected from tracking and\nfingerprinting at <a href=\"https://coveryourtracks.eff.org/\">Cover Your Tracks</a>.</p>\n<p>Also check out, <a href=\"https://amiunique.org/fingerprint\">Am I Unique</a></p>\n<blockquote>\n<p>⚠️ WARNING: Don’t put too much weight into the results as people often check\ntheir fingerprint, change one metric and check it again over and over skewing\nthe results. It is helpful for knowing the fingerprint values that trackers\ntrack.</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://forum.torproject.org/t/browser-fingerprinting/1228/25\">Browser Fingerprinting Tor Forum</a></p>\n</li>\n<li>\n<p><a href=\"https://madaidans-insecurities.github.io/browser-tracking.html\">Madaidans Hot Take on Browser Tracking</a></p>\n</li>\n</ul>\n<hr />\n<h3>Browsers</h3>\n<p>I currently run NixOS in a VM with a secureblue host,\n<a href=\"https://github.com/secureblue/Trivalent\">Trivalent</a> is my default browser, a\nsecurity-focused, Chromium-based browser.</p>\n<p>In this section, the goal is to outline several browser options on NixOS and\nshow concrete configurations (with a focus on LibreWolf that can be adapted to\nFirefox). Browsers are complicated and people have different security, privacy,\nand usability needs, so this is not an endorsement of a single “best” choice.\nInstead, the following subsections describe trade-offs and example setups so you\ncan decide which browser and configuration best match your own threat model and\nworkflow.</p>\n<p>IMO there aren’t many Chromium-based browsers on NixOS that hit the sweet spot\nof being both security-forward and privacy-respecting. Many of the “privacy\nbrowser” options you’ll see on NixOS are Firefox forks; with the right tweaks\nthey can be made pretty private, but they generally don’t match Chromium-family\nbrowsers on exploit mitigations and sandbox depth. ​</p>\n<h4>Brave</h4>\n<p>Brave is basically “Chromium, but with privacy features turned on by default,”\nand the big win is that most of the protection comes from the browser itself\n(Shields + anti-fingerprinting) instead of from a pile of extensions. Brave’s\nfingerprinting story is also unusually practical: instead of trying to make\nevery Brave user look identical, it mixes API blocking with per-site/per-session\nrandomization (“farbling”) so the fingerprint is harder to reuse across\ncontexts.</p>\n<p>On NixOS, IMO the best available Chromium-based browser is Brave. Brave strips\nout Google’s tracking code and includes a native add/tracker blocker (Brave\nShields). It also ships a best‑effort anti‑fingerprinting system that combines\n(1) blocking/removing/modifying certain high-signal APIs and (2) “privacy\nthrough randomization” (farbling), where it returns slightly altered values so\nfingerprints don’t stay stable across sites/sessions. ​</p>\n<p><strong>Things to Note about Brave</strong>:</p>\n<ul>\n<li>\n<p>Shields is a bundle of controls (trackers/ads, cookies, fingerprinting\ndefenses, HTTPS upgrades, referrer/query stripping, storage cleanup), so\nturning Shields off for a broken site is a big privacy downgrade for that\nsite—not just “adblock off.” ​</p>\n</li>\n<li>\n<p>Brave explicitly recommends validating fingerprinting defenses with realistic\ntests (new private window, restart browser, different profile, clear site\nstorage) and expects the fingerprint to change across those boundaries. ​</p>\n</li>\n<li>\n<p>Extension bloat will break Brave’s privacy and security story: each extension\nincreases attack surface and usually has broad privileges, while Brave’s core\npitch is that you can get most of the privacy wins without third-party code in\nyour browser process</p>\n</li>\n</ul>\n<p><strong>Drawbacks worth mentioning</strong>:</p>\n<ul>\n<li>\n<p>Brave is still Chromium/Blink, so using it doesn’t help with engine diversity;\nif Gecko dies, the web becomes even more “whatever Chromium implements,” which\nis a long-term ecosystem risk.</p>\n</li>\n<li>\n<p>Brave ships a lot (Rewards, Wallet, VPN upsells, Leo/AI features depending on\nbuild/channel), and every extra subsystem is more UI complexity and\npotentially more bugs/attack surface than some browsers.</p>\n</li>\n<li>\n<p>Brave’s model assumes the browser does most privacy work; piling on extensions\nincreases privileged code, fingerprint uniqueness, and the chance of a\nmalicious/compromised extension.</p>\n</li>\n<li>\n<p>Farbling and API defenses reduce stability/linkability, but sophisticated\ntrackers can still correlate behavior, logins, IP ranges, and high-level\npatterns—so “I enabled anti-fingerprinting” shouldn’t be read as “I can’t be\ntracked.”</p>\n</li>\n<li>\n<p>Brave’s built-in ad system (Rewards) is opt-in, but Brave also promotes\nfeatures like Rewards and can show sponsored content (e.g., sponsored new-tab\nimages) unless you disable/hide it</p>\n</li>\n</ul>\n<h4>Firefox</h4>\n<p>Firefox is kind of its own thing: it runs on Gecko, not on Chromium or WebKit,\nso it’s one of the only mainstream browsers that isn’t just another Chrome fork.\nThat uniqueness matters if you care about engine diversity and not having the\nentire web effectively dictated by a single vendor. It’s also very tweakable, so\nif you’re willing to flip some prefs and add a couple of key extensions, you can\nturn it into a solid privacy‑focused daily driver without giving up a\nnon‑Chromium stack.</p>\n<p>Firefox will usually get security fixes sooner than any fork, and some forks lag\nbehind on patching, which can leave known vulnerabilities exploitable for\nlonger. If you use Firefox’s built‑in Enhanced Tracking Protection (ETP), Resist\nFingerprinting (RFP), and hardening templates like ghacks or Arkenfox together\nwith uBlock Origin configured for dynamic filtering, you can replicate what used\nto require a pile of separate extensions.</p>\n<h4>Site Isolation &amp; Firefox Links</h4>\n<p>Firefox does implement Site Isolation via\n<a href=\"https://wiki.mozilla.org/Project_Fission\">Project Fission</a>, but it’s newer and\nhistorically less mature than Chromium’s site‑per‑process model, and it may not\nbe enabled everywhere by default. To check that it is active, go to\n<code>about:config</code> and ensure both <code>fission.autostart</code> and <code>gfx.webrender.all</code> are\nset to <code>true</code>.</p>\n<p>With uBlock Origin you can disable JavaScript per‑site (similar to NoScript),\nenable a bunch of high‑quality blocklists, and selectively relax rules when a\ntrusted site breaks. To turn on Enhanced Tracking Protection and fingerprinting\nprotections in the UI, go to <code>Settings -&gt; Privacy &amp; Security</code> -&gt;\n<code>Enhanced Tracking Protection -&gt; Custom</code>; if that causes breakage on a\nparticular website, click the shield icon in the URL bar and disable protections\njust for that site.</p>\n<ul>\n<li><a href=\"https://github.com/gorhill/uBlock/wiki\">uBlock Wiki</a></li>\n</ul>\n<p>Once you select <code>Custom</code>, you’ll see that among the options is to block\n<code>Known fingerprinters</code> as well as <code>Suspected fingerprinters</code>. The “Known\nFingerprinters” protection works by blocking scripts listed in\n<a href=\"https://disconnect.me/trackerprotection#categories_of_trackers\">Disconnect’s fingerprinting list</a>\nFor most users they suggest using the above FPP to avoid breakage. To go further\nand enable RFP, go to <code>about:config</code> and set <code>privacy.resistFingerprinting</code> to\n<code>true</code>.</p>\n<details>\n<summary> ✔️ Further Reading on Firefox Defenses </summary>\n<ul>\n<li>\n<p><a href=\"https://support.mozilla.org/en-US/kb/resist-fingerprinting\">Mozilla Resist Fingerprinting</a></p>\n<ul>\n<li>To ensure Site Isolation is enabled, in <code>about:config</code>, set\n<code>fission.autostart</code>, and <code>gfx.webrender.all</code> prefs to <code>true</code>.(It’s disabled\nby default on android).</li>\n</ul>\n</li>\n<li>\n<p><a href=\"https://en.wikipedia.org/wiki/Entropy_(computing)\">Entropy</a>: in this\ncontext, is a measure of how much unique information a specific browser\nfeature contributes to your fingerprint. It’s often quantified in <strong>bits of\nentropy</strong>, where higher bits mean more uniqueness (i.e., easier to identify\nyou).</p>\n<ul>\n<li>A “bit” is a basic unit of information for computers. Entropy measuring\nsites results are measured in “bits of identifying information”.</li>\n</ul>\n</li>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Glossary/Origin\">Origin</a>: Web\ncontent’s <em>origin</em> is defined by the <em>scheme</em> (protocol), <em>hostname</em> (domain),\nand port of the URL used to access it. Two objects have the same origin only\nwhen the scheme, hostname, and port all match.</p>\n</li>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\">Same-origin policy</a>:\nis a critical security mechanism that restricts how a document or script\nloaded by one origin can interact with a resource from another origin. It\nhelps isolate potentially malicious documents, reducing possible attack\nvectors.</p>\n</li>\n<li>\n<p><a href=\"https://blog.mozilla.org/security/2021/05/18/introducing-site-isolation-in-firefox/\">Firefox Site-Isolation</a>.\nFirefox does provide site-isolation as well.</p>\n</li>\n<li>\n<p><a href=\"https://www.mozilla.org/en-US/security/advisories/mfsa2018-01/\">Protection from side-channel attacks</a></p>\n</li>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Insecure_passwords\">MDN Insecure passwords</a></p>\n<ul>\n<li><a href=\"https://blog.mozilla.org/tanvi/2016/01/28/no-more-passwords-over-http-please/\">Risks of reused passwords</a></li>\n</ul>\n</li>\n</ul>\n</details>\n<hr />\n<h2>LibreWolf</h2>\n<p><strong>LibreWolf</strong> is an open-source fork of Firefox with a strong focus on privacy,\nsecurity, and user freedom. LibreWolf enables always HTTPS, includes\nuBlockOrigin, and only includes privacy focused search engines by default.</p>\n<p>Example LibreWolf config implementing many of the STIG recommendations:</p>\n<details>\n<summary> ✔️ Click to expand LibreWolf Example </summary>\n<pre><code class=\"language-nix\"># librewolf.nix\n{pkgs, lib, config, ...}: let\n  cfg = config.custom.librewolf;\nin {\n  options.custom.librewolf = {\n    enable = lib.mkOption {\n      type = lib.types.bool;\n      default = true;\n      description = \"Enable the LibreWolf Module\";\n    };\n  };\n\n  config = lib.mkIf cfg.enable {\n    programs.librewolf = {\n      enable = true;\n      policies = {\n        # A bit annoying\n        DontCheckDefaultBrowser = true;\n        # Pocket is insecure according to DoD\n        DisablePocket = true;\n        # No imperative updates\n        DisableAppUpdate = true;\n      };\n      settings = {\n        # // SV-16925 - DTBF030\n        \"security.enable_tls\" = true;\n        # // SV-16925 - DTBF030\n        \"security.tls.version.min\" = 2;\n        # // SV-16925 - DTBF030\n        \"security.tls.version.max\" = 4;\n\n        # // SV-111841 - DTBF210\n        \"privacy.trackingprotection.fingerprinting.enabled\" = true;\n\n        # // V-252881 - Retaining Data Upon Shutdown\n        \"browser.sessionstore.privacy_level\" = 0;\n\n        # // SV-251573 - Customizing the New Tab Page\n        \"browser.newtabpage.activity-stream.enabled\" = false;\n        \"browser.newtabpage.activity-stream.feeds.section.topstories\" = false;\n        \"browser.newtabpage.activity-stream.showSponsored\" = false;\n        \"browser.newtabpage.activity-stream.feeds.snippets\" = false;\n\n        # // V-251580 - Disabling Feedback Reporting\n        \"browser.chrome.toolbar_tips\" = false;\n        \"browser.selfsupport.url\" = \"\";\n        \"extensions.abuseReport.enabled\" = false;\n        \"extensions.abuseReport.url\" = \"\";\n\n        # // V-251558 - Controlling Data Submission\n        \"datareporting.policy.dataSubmissionEnabled\" = false;\n        \"datareporting.healthreport.uploadEnabled\" = false;\n        \"datareporting.policy.firstRunURL\" = \"\";\n        \"datareporting.policy.notifications.firstRunURL\" = \"\";\n        \"datareporting.policy.requiredURL\" = \"\";\n\n        # // V-252909 - Disabling Firefox Studies\n        \"app.shield.optoutstudies.enabled\" = false;\n        \"app.normandy.enabled\" = false;\n        \"app.normandy.api_url\" = \"\";\n\n        # // V-252908 - Disabling Pocket\n        \"extensions.pocket.enabled\" = false;\n\n        # // V-251555 - Preventing Improper Script Execution\n        \"dom.disable_window_flip\" = true;\n\n        # // V-251554 - Restricting Window Movement and Resizing\n        \"dom.disable_window_move_resize\" = true;\n\n        # // V-251551 - Disabling Form Fill Assistance\n        \"browser.formfill.enable\" = false;\n\n        # // V-251550 - Blocking Unauthorized MIME Types\n        \"plugin.disable_full_page_plugin_for_types\" = \"application/pdf,application/fdf,application/xfdf,application/lso,application/lss,application/iqy,application/rqy,application/lsl,application/xlk,application/xls,application/xlt,application/pot,application/pps,application/ppt,application/dos,application/dot,application/wks,application/bat,application/ps,application/eps,application/wch,application/wcm,application/wb1,application/wb3,application/rtf,application/doc,application/mdb,application/mde,application/wbk,application/ad,application/adp\";\n      };\n    };\n    xdg.desktopEntries.librewolf = {\n      name = \"LibreWolf\";\n      exec = \"${pkgs.librewolf}/bin/librewolf\";\n    };\n    xdg.mimeApps = {\n      enable = true;\n      defaultApplications = {\n        \"text/html\" = \"librewolf.desktop\";\n        \"x-scheme-handler/http\" = \"librewolf.desktop\";\n        \"x-scheme-handler/https\" = \"librewolf.desktop\";\n        \"x-scheme-handler/about\" = \"librewolf.desktop\";\n        \"x-scheme-handler/unknown\" = \"librewolf.desktop\";\n      };\n    };\n  };\n}\n</code></pre>\n<p>And enable it in your <code>home.nix</code> or equivalent with:</p>\n<pre><code class=\"language-nix\"># home.nix\ncustom.librewolf.enable = true;\n</code></pre>\n<p>The <code>xdg</code> settings at the end make LibreWolf the defaults for what is listed.</p>\n<p>Thanks to <code>JosefKatic</code> for putting the above STIG settings in NixOS format.</p>\n<p>Also, go to\n<a href=\"https://accounts.firefox.com/settings#data-collection\">accounts.firefox</a> and\nturn off “Allow Mozilla accounts to send technical and interaction data to\nMozilla”. Also set 2-fa in\n<a href=\"https://accounts.firefox.com/settings#security\">Security Settings</a></p>\n<p>I always set <code>Max Protection</code> for DNS over HTTPS and personally set a custom\nresolver to <code>https://dns.quad9.net/dns-query</code></p>\n<ul>\n<li>Mullvad is also a good option:\n<a href=\"https://mullvad.net/en/help/no-logging-data-policy\">Mullvad no-logging-data-policy</a></li>\n</ul>\n<details>\n<summary> ✔️ Alternative LibreWolf Configuration utilizing Arkenfox </summary>\n<pre><code class=\"language-nix\">{\n  pkgs,\n  lib,\n  config,\n  ...\n}: let\n  cfg = config.custom.librewolf;\nin {\n  options.custom.librewolf = {\n    enable = lib.mkOption {\n      type = lib.types.bool;\n      default = true;\n      description = \"Enable the LibreWolf Module\";\n    };\n  };\n\n  config = lib.mkIf cfg.enable {\n    programs.librewolf = {\n      enable = true;\n      policies = {\n        DontCheckDefaultBrowser = true;\n        DisablePocket = true;\n        DisableAppUpdate = true;\n      };\n      profiles.my-default = {\n        isDefault = true;\n        name = \"Default Profile\";\n        extraConfig = ''\n          ${builtins.readFile ./user.js}\n          \"general.autoScroll\" = true;\n          \"sidebar.verticalTabs\" = true;\n        '';\n\n        settings = {\n        };\n      };\n    };\n    xdg.desktopEntries.librewolf = {\n      name = \"LibreWolf\";\n      exec = \"${pkgs.librewolf}/bin/librewolf\";\n    };\n    xdg.mimeApps = {\n      enable = true;\n      defaultApplications = {\n        \"text/html\" = \"librewolf.desktop\";\n        \"x-scheme-handler/http\" = \"librewolf.desktop\";\n        \"x-scheme-handler/https\" = \"librewolf.desktop\";\n        \"x-scheme-handler/about\" = \"librewolf.desktop\";\n        \"x-scheme-handler/unknown\" = \"librewolf.desktop\";\n      };\n    };\n  };\n}\n</code></pre>\n<p>Download the\n<a href=\"https://github.com/arkenfox/user.js/blob/master/user.js\">Arkenfox user.js</a> and\nreview it making sure that you agree with the settings. If you do, place it in\nthe same directory as your <code>librewolf.nix</code>.</p>\n<p>Read the <a href=\"https://github.com/arkenfox/user.js/wiki\">Arkenfox Wiki</a></p>\n<p>The <code>user.js</code> is full of comments and information, read it and adjust it for\nyour needs. The following enables RFP fingerprint protection:</p>\n<pre><code class=\"language-js\">***/ user.js ***/\nuser_pref(\"privacy.resistFingerprinting\", true); // [FF41+]\nuser_pref(\"privacy.resistFingerprinting.pbmode\", true); // [FF114+]\n</code></pre>\n<p>As you learn more, you can get more strict if you so choose.</p>\n<p>Rebuild, launch LibreWolf, and check your <code>~/.librewolf/my-default/user.js</code>. It\nshould match the Arkenfox settings. Initially, only the <code>user.js</code> will be\nlisted, as you run LibreWolf other profile files and folders are created\ndynamically.</p>\n<p>In LibreWolf type <code>Ctrl + Shift + J</code> and look for any errors.</p>\n<p>Type <code>about:config</code> into the address bar and search a few of the settings that\nArkenfox changes, do they match?</p>\n<p>The <code>user.js</code> is read <strong>in order</strong>, if there are 2 of the same setting, the last\none will be applied. Adding overrides to the settings attribute above places the\nchanges at the <strong>beginning</strong> of the <code>user.js</code> which isn’t what we want. Placing\nthem after the <code>${builtins.readFile ./user.js}</code> in <code>extraConfig</code> amends them to\nthe <strong>end</strong> of the <code>user.js</code> allowing us to override the defaults.</p>\n<p>The process is the same with Firefox but since Arkenfox strongly recommends\nUblock Origin and it is built into LibreWolf it makes sense to use the browser\nwith the stronger defaults.</p>\n<blockquote>\n<p>❗ NOTE: There is a home-manager module called <code>arkenfox-nixos</code> that is\nsupposed to make updates easier but IMO the documentation leaves you guessing\nhow to use it. As updates come in to Firefox/LibreWolf some of the settings\nbecome unnecessary so it’s important to keep an eye on both Firefox and\nArkenfox updates. Which both have RSS feeds that will alert you upon changes.</p>\n</blockquote>\n<p>I personally use <a href=\"https://feeder.co/\">Feeder</a> as my open-source RSS feed reader,\navailable in most app stores including F-Droid. It is listed on\n<a href=\"https://www.privacytools.io/privacy-rss-feed-readers\">PrivacyTools</a>.</p>\n<ul>\n<li>\n<p><a href=\"https://github.com/arkenfox/user.js/commits/master.atom\">Arkenfox Recent Commits RSS feed</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/arkenfox/user.js/releases.atom\">Arkenfox Release Notes RSS</a></p>\n</li>\n<li>\n<p><a href=\"https://www.mozilla.org/en-US/firefox/nightly/notes/feed/\">Firefox Nightly release notes</a></p>\n</li>\n</ul>\n</details>\n<h4>Search Defaults</h4>\n<p><strong>Startpage</strong>: Advertised as the world’s most private search engine. “Startpage\ndelivers Google search results via their proprietary personal data protection\ntechnology.”</p>\n<ul>\n<li>\n<p><a href=\"https://www.startpage.com/\">Startpage</a></p>\n</li>\n<li>\n<p>To add Startpage as a search engine, add\n<code>https://www.startpage.com/sp/search?query=%s</code>.</p>\n</li>\n</ul>\n<p><strong>SearXNG</strong> an open-source, privacy-respecting metasearch engine that aggregates\nresults from various search services, such as Google, DuckDuckGo, etc. without\ntracking you or profiling your searches. You can add SearXNG to firefox by going\nto <code>about:preferences#search</code> and at the bottom click <code>Add</code>, URL will be\n<code>https://searx.be/search?q=%s</code>.</p>\n<blockquote>\n<p>❗️ NOTE: SearXNGs google results are not working as of 11-17-25 and haven’t\nfor a while now leading to bad results being returned for most instances. It’s\nmy understanding this is because Google is actively blocking automated\nrequests from SearXNG. Devs sometimes publish patches or workarounds, but\nthese are quickly blocked when Google changes their back-end.</p>\n</blockquote>\n<blockquote>\n<p>❗️ NOTE: The above searx is the default and doesn’t give many relevant\nresults. To get relevant results find a\n<a href=\"https://searx.space/\">public instance</a> with a good rating from your area and\nadd the <code>search?q=%s</code> to the end of it. For example, I’m using\n<code>https://priv.au/search?q=%s</code>.</p>\n</blockquote>\n<p>Searx is a bit different, you can choose which search engine you want for your\ncurrent search with <code>!ddg search term</code> to use duckduckgo for example.</p>\n</details>\n<h4>Tor Browser</h4>\n<blockquote>\n<p>❗ NOTE: Tor is <strong>not</strong> the most secure browser, anonymity and security can\noften be at odds with each other. Having the exact same browser as many other\npeople isn’t the best security practice, but it is great for anonymity. Tor is\nalso based on Firefox Esr, which only receives patches for vulnerabilities\nconsidered Critical or High which can be taken advantage of.</p>\n</blockquote>\n<p>Tor is a modified version of Firefox specifically designed for use with Tor.</p>\n<p>Tor routes your internet traffic through a global volunteer-operated network,\nmasking your IP address and activities from local observers, ISPs, websites, and\nsurveillance systems. This helps you protect personal information and maintain\nanonymity when browsing, communicating, or using online services.</p>\n<p>Adding browser plugins to Tor can de-anonymize you, don’t do it. Tor is already\nbuilt with the necessary plugins and privacy protecting rules, so adding more is\nunnecessary and actually dangerous for your anonymity.</p>\n<p>A Tor exit node can easily see your traffic, and if you’re not using HTTPS then\nit may be able to modify that traffic. Only use HTTPS when browsing the clear\nnet with Tor, this doesn’t apply to onion services (<code>.onion</code>) as the traffic\nstays inside the Tor network all the way to the destination.</p>\n<p>You can visit both the clear web and <code>.onion</code> sites on Tor. Whenever possible\nyou should utilize Onion Services (<code>.onion</code> addresses) so communications and web\nbrowsing stay within the Tor network. <code>.onion</code> URLS form a tunnel that is\nend-to-end encrypted using a random rendezvous point and incorporating\n<a href=\"https://en.wikipedia.org/wiki/Forward_secrecy\">perfect forward secrecy (PFS)</a>.</p>\n<p>Bridges are only necessary in countries that don’t allow people to use Tor.\nUsing Bridges when they aren’t needed takes resources away from people in\noppressive regimes that need, only use them if necessary. Read the guides, and\nuse Tails OS, or Whonix when it really matters.</p>\n<ul>\n<li><a href=\"https://saylesss88.github.io/nix/whonix_kvm.html\">Whonix KVM on NixOS</a></li>\n</ul>\n<p>You will see a lot of conflicting information about using Tor with a VPN. If you\nare in an area that blocks access to Tor or it is dangerous to use Tor, by all\nmeans use a trusted VPN.</p>\n<h3>TorPlusVPN</h3>\n<ul>\n<li>\n<p><a href=\"https://gitlab.torproject.org/legacy/trac/-/wikis/doc/TorPlusVPN\">Tor Project Wiki TorPlusVPN</a></p>\n</li>\n<li>\n<p><a href=\"https://www.privacyguides.org/en/advanced/tor-overview/#safely-connecting-to-tor\">Safely Connecting to Tor</a></p>\n</li>\n</ul>\n<p><strong>Learn about Tor</strong></p>\n<p>I recommend starting with\n<a href=\"https://www.privacyguides.org/articles/2025/04/30/in-praise-of-tor/#onion-sites-you-can-visit-using-the-tor-browser\">Privacy Guides In Praise of Tor</a>\nand then reading their\n<a href=\"https://www.privacyguides.org/en/advanced/tor-overview/\">Tor Overview</a> they\nhave been the most informative resources I’ve come across yet.</p>\n<p>The Electronic Frontier Foundation sponsors and helps fund Tor and so does the\nUnited States Government.</p>\n<p>If you are fortunate to live outside of oppressive regimes with extreme\ncensorship, using Tor for every day, mundane activities is likely safe and won’t\nput you on any harmful “list.” Even if it did, you’d be in good company—these\nlists mostly contain great people working tirelessly to defend human rights and\nonline privacy worldwide.</p>\n<p>By using Tor regularly for ordinary browsing, you help strengthen the network,\nmaking it more robust and anonymous for everyone. This collective support makes\nstaying private easier for activists, journalists, and anyone facing online\nsurveillance or censorship. The writer of the PrivacyGuides article mentions\nusing Tor when he needs to access Google Maps to protect his privacy</p>\n<p>So, consider embracing Tor not only for sensitive browsing but also for daily\nroutine tasks. Every user adds valuable noise to the network, helping protect\nprivacy and freedom for all.</p>\n<p><strong>Tor is at risk, and needs our help</strong>. Despite its strength and history, Tor\nisn’t safe from the same attacks oppressive regimes and misinformed legislators\ndirect at encryption and many other privacy-enhancing\ntechnologies.–<a href=\"https://www.privacyguides.org/articles/2025/04/30/in-praise-of-tor/#how-to-support-tor\">How to Support Tor</a></p>\n<ul>\n<li><a href=\"https://wiki.nixos.org/wiki/Tor\">Tor on NixOS</a>\n<ul>\n<li>\n<p><a href=\"https://tb-manual.torproject.org/\">Tor Browser User Manual</a></p>\n</li>\n<li>\n<p><a href=\"https://support.torproject.org/faq/staying-anonymous/\">Tor staying-anonymous</a></p>\n</li>\n<li>\n<p><a href=\"https://ssd.eff.org/module/how-to-use-tor\">How to Use Tor</a></p>\n</li>\n<li>\n<p><a href=\"https://torproject.github.io/manual/secure-connections/\">Cool Graphic Showing Secure Connections with Tor</a></p>\n</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h4>Mullvad-Browser</h4>\n<p>Rather than try to tweak a browser into fingerprinting submission, I recommend\nusing either Tor or Mullvad-Browser when fingerprintability is the highest\nissue. Both Tor and Mullvad-Browser were designed specifically for this purpose\nand you likely won’t get as much out of tweaking another browser.</p>\n<p>Mullvad-Browser is free and open-source and was developed by the Tor Project in\ncollaboration with Mullvad VPN.(Another Firefox Derivative). It is also the top\nrecommended browser from PrivacyGuides.</p>\n<p>It is the Tor Browser without the Tor Network, allowing you to use the privacy\nfeatures Tor created along with a VPN if you so choose.</p>\n<ul>\n<li><a href=\"https://mullvad.net/en/browser\">Mullvad-Browser</a>, is in Nixpkgs as:\n<code>pkgs.mullvad-browser</code></li>\n</ul>\n<hr />\n<h2>Making Your Browser Amnesic, the Nix Way</h2>\n<p><strong>Problem</strong>: Browsers leak data via <code>.cache</code> and <code>.config</code>.</p>\n<p><strong>Pro Tip</strong>: Even if you use a persistent home directory, you should mount your\n<code>~/.cache</code> folder to <code>tmpfs</code>.</p>\n<ul>\n<li>\n<p><strong>Performance</strong>: Browsers perform thousands of small read/writes to the cache.\nIn-memory storage is significantly faster.</p>\n</li>\n<li>\n<p><strong>Disk Health</strong>: It prevents “SSD wear” from constant caching of temporary web\nassets.</p>\n</li>\n<li>\n<p><strong>Forensic Hygiene</strong>: It ensures that volatile “junk” like images, scripts,\nand stylesheets never touches your physical platter.</p>\n</li>\n</ul>\n<blockquote>\n<p><strong>Note</strong>: This will <strong>not</strong> wipe your browser session (tabs, cookies,\nhistory), as those are stored in <code>~/.config</code>. If you want a truly “amnesic”\nbrowsing session that wipes everything on reboot, you must also mount your\nbrowser’s profile directory (e.g., <code>~/.mozilla</code> or <code>~/.config/BraveSoftware</code>)\nto <code>tmpfs</code>.</p>\n</blockquote>\n<pre><code class=\"language-nix\">fileSystems.\"/home/youruser/.cache\" = {\n  device = \"none\";\n  fsType = \"tmpfs\";\n  options = [ \"size=4G\" \"mode=777\" ];\n};\n</code></pre>\n<p>Ensure it was applied with:</p>\n<pre><code class=\"language-bash\">findmnt /home/youruser/.cache\n</code></pre>\n<p>You can apply the same <code>tmpfs</code> logic to your config folder. <strong>Warning</strong>: This\nwill wipe your settings, extensions, and history every reboot.</p>\n<p>Manual <code>tmpfs</code> mounts in <code>configuration.nix</code> are powerful because they happen at\nthe system level, but they require explicit ownership (<code>uid</code>/<code>gid</code>) to work with\nuser-level applications.</p>\n<pre><code class=\"language-nix\">fileSystems.\"/home/youruser/.config/BraveSoftware\" = {\n  device = \"none\";\n  fsType = \"tmpfs\";\n  options = [\n  \"size=4G\"\n  \"mode=700\" # Use 700 for privacy\n  \"noatime\"\n  # Replace `1000` with the output of `id -u`\n  \"uid=1000\"\n  # Replace `100` with output of `id -g`\n  \"gid=100\"\n  ];\n};\n</code></pre>\n<p><strong>How to find your UID/GID</strong></p>\n<pre><code class=\"language-bash\">id -u &amp;&amp; id -g\n</code></pre>\n<p>Replace the above <code>uid=</code>, and <code>gid=</code> values with the output of the above\ncommand.</p>\n<hr />\n<details>\n<summary> ✔️ Example Script to wipe cache and generate new `machine-id` </summary>\n<p>If you followed the above “Nix way” of Amnesic cache, the following script is\nunnecessary, I’m leaving it here for now for those that are interested in\nchanging their machine-id imperatively.</p>\n<ul>\n<li>\n<p><a href=\"https://www.man7.org/linux/man-pages/man5/machine-id.5.html\">man page machine-id(5)</a></p>\n</li>\n<li>\n<p>The following example is adapted from\n<a href=\"https://firejail.wordpress.com/all-about-tor/\">Firejail All About Tor</a>\nsection, adapted for NixOS.</p>\n</li>\n</ul>\n<p>Save the following script as <code>cleanup.sh</code>, change <code>Your-User</code> to your username:</p>\n<pre><code class=\"language-bash\">#!/bin/sh -e\nUSER=\"Your-User\"\nHOME_DIR=\"/home/$USER\"\n# clear user cache directly as root\nsudo -u \"$USER\" rm -fr \"$HOME_DIR/.cache\"\n# generate a new machine-id\nrm -f /var/lib/machine-id\ndbus-uuidgen &gt; /var/lib/machine-id\ncp /var/lib/machine-id /etc/machine-id\nchmod 444 /etc/machine-id\nexit 0\n</code></pre>\n<p>The <code>~/.cache</code> directory is where most programs store runtime information:\nwebpages you visited, torrent trackers you connected to, and deleted emails.\nIt’s a good idea to remove them at shutdown. –Firejail all-about-tor</p>\n<p>Check <code>/etc/machine-id</code> &amp; <code>~/.cache</code> before running the script:</p>\n<pre><code class=\"language-bash\">cat /etc/machine-id\n# Output\n0b46feb27a20469da0ee62baaeb51c5c\nls ~/.cache\n</code></pre>\n<pre><code class=\"language-bash\">chmod +x cleanup.sh\nsudo ./cleanup.sh\n</code></pre>\n<p>Recheck your <code>machine-id</code> and <code>~/.cache</code> directories, you should have a newly\ngenerated <code>machine-id</code> and minimal files in the <code>~/.cache</code> directory. The\nFirejail example shows a systemd unit that runs the above script at every\nshutdown but that may be overkill, I suggest running it occasionally to make it\nharder for sites to link your <code>machine-id</code> to you.</p>\n</details>\n<p>Privacy protection doesn’t need to be perfect to make a difference. The best\nprotection against tracking and fingerprinting available is to use Tor. Many\nadd-ons are redundant, do some research and avoid using an add-on for something\nthat can be accomplished with built-in settings.</p>\n<ul>\n<li><a href=\"https://ssd.eff.org/module/how-to-use-tor\">Surveillance Self-Defense How to: Use Tor</a></li>\n</ul>\n<p>There are more hardening parameters that can be set but this should be a good\nstarting point for a hardened version of LibreWolf. When testing with Cover your\ntracks, customized LibreWolf tested as having stronger tracking protection than\ndefault Mullvad-Browser and NoScript significantly cuts down the data available\nfor fingerprinting by disabling JavaScript.</p>\n<ul>\n<li>The <a href=\"https://wiki.garudalinux.org/en/privacy-guide\">Garuda Privacy-Guide</a> has\ngood tips and recommendations for browser add-ons.</li>\n</ul>\n<hr />\n<h3>Virtual Private Networks (VPNs)</h3>\n<p>A <strong>VPN</strong> (Virtual Private Network) encrypts your Internet connection and routes\nyour traffic through a VPN provider’s servers, masking your IP address from\nlocal network observers, ISPs, and websites. Using a VPN can prevent your ISP or\nlocal Wi-Fi owner from tracking what sites you visit (they only see a connection\nto the VPN), and can help circumvent some regional restrictions or filtering.</p>\n<p>However, VPNs simply shift your trust: Instead of your ISP seeing your activity,\nyour VPN provider can, so you must trust their privacy policies and\ninfrastructure. Quality and privacy protections vary widely from one VPN company\nto another.</p>\n<p>I see over and over again that Mullvad VPN is the best, I am in no way\naffiliated with them this is just what I hear. They allow you to pay with cash\ncompletely anonymously and keep very minimal metadata. Metadata is a big deal,\nthe US gov has admitted to killing people based solely on their metadata.</p>\n<p>Your ISP almost certainly does sketchy stuff with your data, personally I would\nrather trust a company like Mullvad whose whole reputation is based on their\ntrustworthiness, transparency, and data protection.</p>\n<p>You can use a VPN with Tor, but it’s not recommended by the Tor Project unless\nyou’re an advanced user who knows how to configure both in a way that doesn’t\ncompromise your privacy.</p>\n<p><strong>Popular VPNs on NixOS</strong></p>\n<ul>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Mullvad_VPN\">Mullvad VPN</a> Mullvad VPN uses\nWireGuard under the hood and only works if <code>systemd-resolvd</code> is enabled.</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/WireGuard\">WireGuard VPN</a>, WireGuard is a\nprotocol, but also a VPN provider on NixOS.</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Tailscale\">Tailscale</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/OpenVPN\">OpenVPN</a>, OpenVPN is both a protocol and\nfull-featured VPN provider on NixOS.</p>\n</li>\n</ul>\n",
      "date_published": "2025-12-21T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/vcs/jujutsu.html",
      "url": "https://saylesss88.github.io/vcs/jujutsu.html",
      "title": "JJ VCS",
      "content_html": "<h1>Version Control with JJ</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/../images/jujutsu.png\" alt=\"JJ Logo\" /></p>\n<div style=\"font-size: 0.8em; margin-top: 10px;\">\n  **Image Source:** This image is from the [Jujutsu VCS repository](https://github.com/jj-vcs/jj) and is licensed under the Apache 2.0 License.\n</div>\n<p>⚠️ <strong>Security Reminder</strong>: Never commit secrets (passwords, API keys, tokens,\netc.) in plain text to your Git repository. If you plan to publish your NixOS\nconfiguration, always use a secrets management tool like <code>sops-nix</code> or <code>agenix</code>\nto keep sensitive data safe. See the\n<a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">Sops-Nix Guide</a>\nfor details.</p>\n<h2>Getting Started</h2>\n<p>Jujutsu (jj) is a modern, Git-compatible version control system designed to\nsimplify and improve the developer experience. It offers a new approach to\ndistributed version control, focusing on a more intuitive workflow, powerful\nundo capabilities, and a branchless model that reduces common pitfalls of Git.</p>\n<p><strong>Recommended resources</strong>:</p>\n<ul>\n<li>\n<p><a href=\"https://steveklabnik.github.io/jujutsu-tutorial/\">Steve’s Jujutsu Tutorial</a>\n(most up to date). Steve does an excellent job explaining the ins and outs of\nJujutsu.</p>\n</li>\n<li>\n<p><a href=\"https://zerowidth.com/2025/jj-tips-and-tricks/\">zerowidth jj-tips-and-tricks</a></p>\n</li>\n<li>\n<p>Official:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">jj help -k tutorial\n</code></pre>\n<ul>\n<li>\n<p>Every time you run a <code>jj</code> command, it examines the working copy and takes a\nsnapshot.</p>\n</li>\n<li>\n<p>Command help:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">jj &lt;command&gt; --help\njj git init --help\njj git push --help\n</code></pre>\n<h2>🔑 Key Concepts</h2>\n<details>\n<summary> ✔️ Click to Expand Key Concepts </summary>\n<ol>\n<li><strong>Working Copy as Commit</strong></li>\n</ol>\n<ul>\n<li>\n<p>In JJ your working copy is always a real commit. Any changes you make are\nautomatically recorded in this working commit. The working copy is always\n(<code>@</code>) and the Parent commit is always <code>(@-)</code> keep this in mind.</p>\n</li>\n<li>\n<p>There is <strong>no staging area</strong> (index) as in Git. You do not need to run\n<code>git add</code> or <code>git commit</code> for every change. Modifications are always tracked\nin the current commit.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li><strong>Branchless Workflow and Bookmarks</strong></li>\n</ol>\n<ul>\n<li>\n<p>JJ does not have the concept of a “current branch.” Instead, use bookmarks,\nwhich are named pointers to specific commits.</p>\n</li>\n<li>\n<p>Bookmarks do not move automatically. Commands like <code>jj new</code> and <code>jj commit</code>\nmove the working copy, but the bookmark stays were it was. Use\n<code>jj bookmark move</code> to move bookmarks. (e.g., <code>jj bookmark move main</code>). You can\nalso use <code>jj bookmark set main -r @</code> to explicitly set the main bookmark to\npoint at the working copy commit.</p>\n</li>\n<li>\n<p>Only commits referenced by bookmarks are pushed to remotes, preventing\naccidental sharing of unfinished work.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li><strong>Automatic Tracking and Simpler Workflow</strong></li>\n</ol>\n<p>To stop tracking a specific file, first add it to your <code>.gitignore</code>, then run\n<code>jj untrack &lt;file&gt;</code></p>\n<ul>\n<li>The working copy acts as a live snapshot of your workspace. Commands first\nsync filesystem changes into this commit, then perform the requested\noperation, and finally update the working copy if needed.</li>\n</ul>\n<ol start=\"4\">\n<li>Operation Log and Undo</li>\n</ol>\n<ul>\n<li>\n<p>JJ records every operation (commits, merges, rebases, etc.) in an <strong>operation\nlog</strong>. Inspect it with: <code>jj op log</code></p>\n</li>\n<li>\n<p>You can view and undo any previous operation, not just the most recent one,\nmaking it easy to recover from mistakes, a feature not present in Git’s core\nCLI.</p>\n</li>\n</ul>\n<ol start=\"5\">\n<li>First-Class Conflict Handling</li>\n</ol>\n<p>Conflicts happen when JJ can’t figure out how to merge different changes made to\nthe same file.</p>\n<ul>\n<li>\n<p>Conflicts are stored inside commits, not just in the working directory. You\ncan resolve them at any time, not just during a merge or rebase.</p>\n</li>\n<li>\n<p>Conflict markers are inserted directly into files, and JJ can reconstruct the\nconflict state from these markers. You can resolve conflicts by editing the\nfiles or using <code>jj resolve</code>.</p>\n</li>\n</ul>\n<ol start=\"6\">\n<li>Revsets and Filesets</li>\n</ol>\n<ul>\n<li>\n<p><strong>Revsets</strong>: JJ’s powerful query language for selecting sets of commits,\ninspired by Mercurial. For example, <code>jj log -r \"author(alice) &amp; file(*.py)\"</code>\nlists all commits by Alice that touch Python files.</p>\n</li>\n<li>\n<p><strong>Filesets</strong>:JJ supports a functional language for selecting sets of files,\nallowing advanced file-based queries and operations.</p>\n</li>\n</ul>\n<table><thead><tr><th style=\"text-align: left\">Feature</th><th style=\"text-align: left\">Git</th><th style=\"text-align: left\">Jujutsu (jj)</th></tr></thead><tbody>\n<tr><td style=\"text-align: left\">Staging Area</td><td style=\"text-align: left\">Yes (git add/index)</td><td style=\"text-align: left\">No, working copy is always a commit</td></tr>\n<tr><td style=\"text-align: left\">Commit Workflow</td><td style=\"text-align: left\">Stage → Commit</td><td style=\"text-align: left\">All changes auto-recorded in working commit</td></tr>\n<tr><td style=\"text-align: left\">Branches</td><td style=\"text-align: left\">Central to workflow</td><td style=\"text-align: left\">Optional, bookmarks used for sharing</td></tr>\n<tr><td style=\"text-align: left\">Undo/Redo</td><td style=\"text-align: left\">Limited, complex</td><td style=\"text-align: left\">Easy, operation log for undo</td></tr>\n<tr><td style=\"text-align: left\">Conflict Handling</td><td style=\"text-align: left\">Manual, can be confusing</td><td style=\"text-align: left\">Conflicts tracked in commits, easier to fix</td></tr>\n<tr><td style=\"text-align: left\">Integration with Git</td><td style=\"text-align: left\">Native</td><td style=\"text-align: left\">Fully compatible, can switch back anytime</td></tr>\n</tbody></table>\n<ol start=\"7\">\n<li>Anonymous branches: In Git a branch is a pointer to a commit that needs a\nname.</li>\n</ol>\n<p>If you haven’t taken the time to deep dive Git, it may be a good time to learn\nabout a new way of doing Version Control that is actually less complex and\neasier to mentally map out in my opinion.</p>\n<p>Jujutsu is a new front-end to Git, and it’s a new design for distributed version\ncontrol. –jj init</p>\n<p>You can use jujutsu (jj) with existing Git repositories with one command.\n<code>jj git init --colocate</code> or <code>jj git init --git-repo /path/to/git_repository</code>.\nThe native repository format for jj is still a work in progress so people\ntypically use a <code>git</code> repository for backend.</p>\n<p>Unlike <code>git</code>, <code>jj</code> has no index “staging area”. It treats the working copy as an\nactual commit. When you make changes to files, these changes are automatically\nrecorded to the working commit. There’s no need to explicitly stage changes\nbecause they are already part of the commit that represents your current working\nstate.</p>\n</details>\n<p><strong>Simplified Workflow</strong></p>\n<p>Check where you’re at, JJ doesn’t care about commits without descriptions but\nGit and GitHub do:</p>\n<pre><code class=\"language-bash\">❯  jj st\nThe working copy has no changes.\nWorking copy  (@) : n a8b19ca2 (empty) (no description set)\nParent commit (@-): k 8c487558 edit(jj): ui.color = always diff.format color-words\n</code></pre>\n<p>We can see that the Working copy is <code>(empty)</code> and has <code>(no description set)</code>,\nlets give it a description:</p>\n<pre><code class=\"language-bash\">❯  jj desc -m \"chore: nix flake update\"\nWorking copy  (@) now at: n 5c36a33d (empty) chore: nix flake update\nParent commit (@-)      : k 8c487558 edit(jj): ui.color = always diff.format color-words\n</code></pre>\n<p>I ran <code>nix flake update</code>, let’s check our status:</p>\n<pre><code class=\"language-bash\">❯  jj st\nWorking copy changes:\nM flake.lock\nWorking copy  (@) : n d54ab019 chore: nix flake update\nParent commit (@-): k 8c487558 edit(jj): ui.color = always diff.format color-words\n</code></pre>\n<ul>\n<li>We can see that running <code>nix flake update</code> modified <code>M</code> our <code>flake.lock</code>. To\nfinalize this change we can run <code>jj new</code>.</li>\n</ul>\n<pre><code class=\"language-bash\">❯  jj new\nWorking copy  (@) now at: v cad9d50b (empty) (no description set)\nParent commit (@-)      : n d54ab019 chore: nix flake update\n</code></pre>\n<p>Now, looking at the output of <code>jj new</code> above, we can see that the Working copy\nis empty and has no description set. If we want to push these changes to GitHub,\nwe have to point the <code>main</code> bookmark where the changes exist, the Parent commit\nin this case:</p>\n<pre><code class=\"language-bash\">❯  jj bookmark set main -r @-\nMoved 1 bookmarks to n d54ab019 main* | chore: nix flake update\n</code></pre>\n<ul>\n<li>Notice the <code>main*</code>, the <code>*</code> indicates that our local <code>main</code> has changes that\n<code>main@origin</code> does not have.</li>\n</ul>\n<p>Ok, our <code>main</code> bookmark is now pointing at our latest changes. We can now run\n<code>jj git push</code> to push them to the remote and make them a part of the permanent\nrecord:</p>\n<pre><code class=\"language-bash\">❯  jj git push\nChanges to push to origin:\n  Move forward bookmark main from 3956b1386d0a to d54ab0197bef\ngit: Enumerating objects: 14, done.\ngit: Counting objects: 100% (14/14), done.\ngit: Delta compression using up to 16 threads\ngit: Compressing objects: 100% (10/10), done.\ngit: Writing objects: 100% (10/10), 1.52 KiB | 520.00 KiB/s, done.\ngit: Total 10 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)\nremote: Resolving deltas: 100% (7/7), completed with 4 local objects.\n</code></pre>\n<p>Success! Let’s check out our status again:</p>\n<pre><code class=\"language-bash\">❯  jj st\nThe working copy has no changes.\nWorking copy  (@) : v cad9d50b (empty) (no description set)\nParent commit (@-): n d54ab019 main | chore: nix flake update\n</code></pre>\n<ul>\n<li>Notice that <code>main*</code> is now just <code>main</code>, indicating our local and remotes are\nin sync!</li>\n</ul>\n<p>Let’s check out the log:</p>\n<pre><code class=\"language-bash\">❯  jj log\n@  v sayls8@proton.me 2026-03-22 12:35:34 5835b760\n│  (no description set)\n◆  n sayls8@proton.me 2026-03-22 12:26:20 main d54ab019\n│  chore: nix flake update\n~\n</code></pre>\n<ul>\n<li>The <code>◆</code> indicates that change <code>n</code> is now immutable after the push. Since it is\nnow immutable, <code>jj</code> automatically creates a new change on top of <code>main</code> and\nmoves the working copy to it.</li>\n</ul>\n<p>This is the hardest part for most people to grasp so let’s try another example\nwhere this time we push changes from the working copy.</p>\n<p>I’ll add a simple <code>README.md</code> to our flake root:</p>\n<pre><code class=\"language-bash\">❯ touch README.md\n\n\n❯  jj st\nWorking copy changes:\nA README.md\nWorking copy  (@) : v 5835b760 (no description set)\nParent commit (@-): n d54ab019 main | chore: nix flake update\n</code></pre>\n<p>Let’s give the change a description. Remember that <code>jj</code> commands default to the\nworking copy, so <code>jj desc</code> is the same is <code>jj desc -r @</code></p>\n<pre><code class=\"language-bash\">❯  jj desc -m \"chore: add README\"\nWorking copy  (@) now at: v cdbb489f chore: add README\nParent commit (@-)      : n d54ab019 main | chore: nix flake update\n</code></pre>\n<p>Now rather than finalizing the current change with <code>jj new</code>, we will just point\nthe <code>main</code> bookmark at the working copy <code>@</code> and then push.</p>\n<pre><code class=\"language-bash\">❯  jj bookmark set main -r @\nMoved 1 bookmarks to v cdbb489f main* | chore: add README\n\n  flake   HEAD [!]\n❯  jj git push\nChanges to push to origin:\n  Move forward bookmark main from d54ab0197bef to cdbb489fecc0\n  git: Enumerating objects: 4, done.\n  git: Counting objects: 100% (4/4), done.\n  git: Delta compression using up to 16 threads\n  git: Compressing objects: 100% (2/2), done.\n  git: Writing objects: 100% (3/3), 332 bytes | 332.00 KiB/s, done.\n  git: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)\n  remote: Resolving deltas: 100% (1/1), completed with 1 local object.\n  Warning: The working-copy commit in workspace 'default' became immutable, so a new commit has been created on top of it.\n  Working copy  (@) now at: s 51306e16 (empty) (no description set)\n  Parent commit (@-)      : v cdbb489f main | chore: add README\n</code></pre>\n<p>With jujutsu, most commands allow you to pass <code>-r</code>/<code>--revision</code></p>\n<h2>What is the Jujutsu Working Copy</h2>\n<details>\n<summary> ✔️ Click To Expand Working Copy Description </summary>\n<p><code>@</code> is a revset for “whichever commit the working copy reflects”. Think of <code>@</code>\nas where you are currently making changes.</p>\n<p>Every time you run a <code>jj</code> command, it examines the working copy (the files on\ndisk) and takes a snapshot. –Steves JJ Tutorial</p>\n<p>Let’s version control an existing nix development environment.</p>\n<pre><code class=\"language-bash\">cd projects/rust\n\n❯  ls\n flake.lock   flake.nix\n\njj git init --colocate\nInitialized repo in \".\"\nHint: Running `git clean -xdf` will remove `.jj/`!\n</code></pre>\n<pre><code class=\"language-bash\">❯  jj log\n@  t sayls8@proton.me 2026-03-15 08:49:21 e8fd7ee0\n│  (no description set)\n◆  z root() 00000000\n</code></pre>\n<p>Let’s give this change a description:</p>\n<pre><code class=\"language-bash\"> jj desc -m \"Initial commit of dev environment\"\nWorking copy  (@) now at: t d671f27c Initial commit of dev environment\nParent commit (@-)      : z 00000000 (empty) (no description set)\n\n❯  jj log\n# The change ID stays the same, but the commit ID changes\n@  t sayls8@proton.me 2026-03-15 09:04:17 d671f27c\n│  Initial commit of dev environment\n◆  z root() 00000000\n</code></pre>\n<p>Ok, I’m done with that change. Let’s start a new one, based off of <code>t</code>:</p>\n<pre><code class=\"language-bash\">jj new\nWorking copy  (@) now at: q a3f5afe8 (empty) (no description set)\nParent commit (@-)      : t d671f27c Initial commit of dev environment\n\njj log\n@  q sayls8@proton.me 2026-03-15 09:10:46 a3f5afe8\n│  (empty) (no description set)\n○  t sayls8@proton.me 2026-03-15 09:04:17 d671f27c\n│  Initial commit of dev environment\n◆  z root() 00000000\n</code></pre>\n<p>Since the repo wasn’t an existing git repo there are no existing branches\n(bookmarks). To share our work we’ll want to create a branch:</p>\n<p>Above is a repo that was just created with <code>jj git init --colocate</code>. Notice that\nthere is already 2 changes with change IDs <code>t</code> &amp; <code>z</code> and 2 commits with\nidentifiers <code>e8fd7ee0</code> &amp; <code>00000000</code>.</p>\n<p>Every <code>jj</code> repo has a root commit with <code>zzzzzzzz</code> <code>00000000 </code> identifiers.\n<code>The diamond </code>◆` represents an immutable, protected revision. This is the\nfoundation of the repo. Jujutsu created a second change based on top of the\nempty root commit.</p>\n<p>The <strong>working copy</strong> in Jujutsu is an actual <strong>commit</strong> that represents the\ncurrent state of the files you’re working on. Unlike Git, where the working copy\nis separate from commits and changes must be explicitly staged and committed, in\nJJ the working copy is a live commit that automatically records changes as you\nmodify files.</p>\n<p>Adding or removing files in the working copy implicitly tracks or untracks them\nwithout needing explicit commands like <code>git add</code></p>\n<p>The working copy commit acts as a snapshot of your current workspace. When you\nrun commands, Jujutsu first syncs the filesystem changes into this commit, then\nperforms the requested operation, and finally updates the working copy if needed</p>\n<p>To finalize your current changes and start a new set of changes, you use the\n<code>jj new</code> command, which creates a new working-copy commit on top of the current\none. This replaces the traditional Git workflow of staging and committing\nchanges separately.</p>\n<p>Conflicts in the working copy are represented by inserting conflict markers\ndirectly into the files. Jujutsu tracks the conflicting parts and can\nreconstruct the conflict state from these markers. You resolve conflicts by\nediting these markers and then committing the resolution in the working copy</p>\n<ul>\n<li>This means that you don’t need to worry about making a change, running\n<code>git add .</code>, running <code>git commit -m \"commit message\"</code> because it’s already\ndone for you. This is handy with flakes by preventing a “dirty working tree”\nand can instantly be rebuilt after making a change.</li>\n</ul>\n</details>\n<h2>Example JJ Module</h2>\n<details>\n<summary> ✔️ Click to Expand JJ home-manager module example </summary>\n<ul>\n<li>\n<p>For <code>lazygit</code> fans, Nixpkgs has <code>lazyjj</code>. I’ve seen that it’s recommended to\nuse jj with <code>meld</code>. I’ll share my <code>jj.nix</code> here for an example:</p>\n</li>\n<li>\n<p>I got a lot of the aliases and such from the\n<a href=\"https://zerowidth.com/2025/jj-tips-and-tricks/\">zerowidth</a> post, this has\nbeen a game changer:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">{\n  lib,\n  config,\n  pkgs,\n  # userVars ? {},\n  #\n  #\n  #\n  ...\n}: let\n  cfg = config.custom.jj;\nin {\n  options.custom.jj = {\n    enable = lib.mkOption {\n      type = lib.types.bool;\n      default = true;\n      description = \"Enable the Jujutsu (jj) module\";\n    };\n\n    userName = lib.mkOption {\n      type = lib.types.nullOr lib.types.str;\n      default = \"sayls8\";\n      description = \"Jujutsu user name\";\n    };\n\n    userEmail = lib.mkOption {\n      type = lib.types.nullOr lib.types.str;\n      default = \"sayls8@proton.me\";\n      description = \"Jujutsu user email\";\n    };\n\n    packages = lib.mkOption {\n      type = lib.types.listOf lib.types.package;\n      default = with pkgs; [lazyjj meld];\n      description = \"Additional Jujutsu-related packages to install\";\n    };\n\n    settings = lib.mkOption {\n      type = lib.types.attrs;\n      default = {\n        ui = {\n          # default-command = \"log-recent\";\n          default-command = [\"status\" \"--no-pager\"];\n          diff-editor = \"gitpatch\";\n          # diff-editor = [\"nvim\" \"-c\" \"DiffEditor\" \"$left\" \"$right\" \"$output\"];\n          # diff-formatter = [\"meld\" \"$left\" \"$right\"];\n          merge-editor = \":builtin\";\n          conflict-marker-style = \"diff\";\n        };\n        git = {\n          # remove the need for `--allow-new` when pushing new bookmarks\n          auto-local-bookmark = true;\n          push-new-bookmarks = true;\n        };\n        revset-aliases = {\n          \"closest_bookmark(to)\" = \"heads(::to &amp; bookmarks())\";\n          \"immutable_heads()\" = \"builtin_immutable_heads() | remote_bookmarks()\";\n          # The following command is incorrect, TODO\n          # \"default()\" = \"coalesce(trunk(),root())::present(@) | ancestors(visible_heads() &amp; recent(), 2)\";\n          \"recent()\" = \"committer_date(after:'1 month ago')\";\n          trunk = \"main@origin\";\n        };\n        template-aliases = {\n          \"format_short_change_id(id)\" = \"id.shortest()\";\n        };\n        merge-tools.gitpatch = {\n          program = \"sh\";\n          edit-args = [\n            \"-c\"\n            ''\n              set -eu\n              rm -f \"$right/JJ-INSTRUCTIONS\"\n              git -C \"$left\" init -q\n              git -C \"$left\" add -A\n              git -C \"$left\" commit -q -m baseline --allow-empty\n              mv \"$left/.git\" \"$right\"\n              git -C \"$right\" add --intent-to-add -A\n              git -C \"$right\" add -p\n              git -C \"$right\" diff-index --quiet --cached HEAD &amp;&amp; { echo \"No changes done, aborting split.\"; exit 1; }\n              git -C \"$right\" commit -q -m split\n              git -C \"$right\" restore . # undo changes in modified files\n              git -C \"$right\" reset .   # undo --intent-to-add\n              git -C \"$right\" clean -q -df # remove untracked files\n            ''\n          ];\n        };\n        aliases = {\n          c = [\"commit\"];\n          ci = [\"commit\" \"--interactive\"];\n          e = [\"edit\"];\n          i = [\"git\" \"init\" \"--colocate\"];\n          tug = [\"bookmark\" \"move\" \"--from\" \"closest_bookmark(@-)\" \"--to\" \"@-\"];\n          log-recent = [\"log\" \"-r\" \"default() &amp; recent()\"];\n          nb = [\"bookmark\" \"create\" \"-r\" \"@-\"]; # new bookmark\n          upmain = [\"bookmark\" \"set\" \"main\"];\n          squash-desc = [\"squash\" \"::@\" \"-d\" \"@\"];\n          rebase-main = [\"rebase\" \"-d\" \"main\"];\n          amend = [\"describe\" \"-m\"];\n          pushall = [\"git\" \"push\" \"--all\"];\n          push = [\"git\" \"push\" \"--allow-new\"];\n          pull = [\"git\" \"fetch\"];\n          dmain = [\"diff\" \"-r\" \"main\"];\n          l = [\"log\" \"-T\" \"builtin_log_compact\"];\n          lf = [\"log\" \"-r\" \"all()\"];\n          r = [\"rebase\"];\n          s = [\"squash\"];\n          si = [\"squash\" \"--interactive\"];\n        };\n        revsets = {\n          # log = \"main@origin\";\n          # log = \"master@origin\";\n        };\n      };\n      description = \"Jujutsu configuration settings\";\n    };\n  };\n\n  config = lib.mkIf cfg.enable {\n    home.packages = cfg.packages;\n\n    programs.jujutsu = {\n      enable = true;\n      settings = lib.mergeAttrs cfg.settings {\n        user = {\n          name = cfg.userName;\n          email = cfg.userEmail;\n        };\n      };\n    };\n  };\n}\n</code></pre>\n<p>In my <code>home.nix</code> I have this to enable it:</p>\n<pre><code class=\"language-nix\">custom = {\n    jj = {\n        enable = true;\n        userName = \"sayls8\";\n        userEmail = \"sayls8@proton.me\";\n        packages = \"\";\n    };\n};\n</code></pre>\n</details>\n<p>The <code>custom.jj</code> module allows me to override the username, email, packages, and\nwhether jj is enabled from a single, centralized place within my Nix\nconfiguration. So only if jj is enabled, <code>lazyjj</code> and <code>meld</code> will be installed.</p>\n<p>With the above <code>gitpatch</code> setup, say you did more work than you want to commit\nwhich is common with jj since it automatically tracks everything. I can now run:</p>\n<pre><code class=\"language-bash\">jj commit -i\n</code></pre>\n<p>And an interactive diff will come up allowing you to choose what to include in\nthe current commit. This also works for <code>jj split -i</code> and <code>jj squash -i</code>.</p>\n<p>Example, using <code>jj commit -i</code>:</p>\n<p><img src=\"https://saylesss88.github.io/../images/jj-gitpatch.png\" alt=\"jj commit -i\" /></p>\n<p>You can also use the <code>jj tug</code> command to make pushing to a remote more\nstraightforward. Since JJ’s bookmarks don’t automatically move as they do with\nGit, you can use <code>jj tug</code> after you’ve made a few commits to move the bookmark\nthat is closest to the parent commit of your current position to your current\ncommit:</p>\n<pre><code class=\"language-bash\">jj tug\njj git push\n</code></pre>\n<p>The <code>tug</code> alias works for both the squash and edit workflows. After running\n<code>jj tug</code>, <code>jj git push</code> should work. If you get an error saying no bookmarks to\nmove, you can run <code>jj new</code> and then run <code>jj tug</code>, this happens when the bookmark\nis already at the parent commit.</p>\n<pre><code class=\"language-nix\"># jj.nix\nmb = [\"bookmark\" \"set\" \"-r\" \"@\"];\n</code></pre>\n<p>Another option would be to run <code>jj mb main</code> before running <code>jj git push</code> in this\nsituation, but you will have to describe the commit first.</p>\n<h2>Issues I’ve Noticed</h2>\n<p><img src=\"https://saylesss88.github.io/../images/jj2.png\" alt=\"jj tree\" /></p>\n<p>I have run into a few issues, such as every flake command reloading every single\ninput every time. <strong>What I mean by this is what you see when you run a flake\ncommand for the first time, it adds all of your flakes inputs.</strong> I believe the\nfix for this is deleting and regenerating your <code>flake.lock</code>. The same thing can\nhappen when you move your flake from one location to another.</p>\n<p>JJ doesn’t seem to automatically track completely new files, running\n<code>git add /file/path.nix</code> enables JJ to start tracking the new file.</p>\n<p>That said, I recommend doing just that after running something like\n<code>jj git init --colocate</code>. Delete your <code>flake.lock</code> and run <code>nix flake update</code>,\n<code>nix flake lock --recreate-lock-file</code> still works but is being depreciated.</p>\n<p>Sometimes the auto staging doesn’t pick up the changes in your configuration so\nrebuilding changes nothing, this has been more rare but happens occasionally.</p>\n<p>One of the most fundamental differences between Jujutsu and Git is how pushing\nworks. If you’re coming from Git, it’s important to understand this shift so you\ndon’t get tripped up by “nothing happened” warnings or missing changes on your\nremote.</p>\n<ul>\n<li>\n<p>In Git, you’re always “on” a branch (e.g., <code>main</code>).</p>\n</li>\n<li>\n<p>When you make a commit, the branch pointer automatically moves forward.</p>\n</li>\n<li>\n<p><code>git push</code> pushes the current branch’s new commits to the remote.</p>\n</li>\n<li>\n<p>If you forget to switch branches, you might accidentally push to the wrong\nplace, but you rarely have to think about “moving” the branch pointer\nyourself.</p>\n</li>\n</ul>\n<p><strong>The JJ Push Model</strong></p>\n<ul>\n<li>\n<p>JJ has no concept of a “currrent branch”</p>\n</li>\n<li>\n<p>Bookmarks <strong>do not</strong> move automatically. When you make a new commit, the\nbookmark (e.g., <code>main</code>) stays where it was. You must explicitly move it to\nyour new commit with <code>jj bookmark set main</code> (or create a new one).</p>\n</li>\n<li>\n<p>JJ only pushes commits that are referenced by bookmarks. If your latest work\nisn’t pointed to by a bookmark, <code>jj git push</code> will do nothing and warn you.\nThis is to prevent accidental pushes and gives you more control over what gets\nshared.</p>\n</li>\n</ul>\n<p><strong>Typical JJ Push Workflow</strong></p>\n<ol>\n<li>Check out where your working copy and Parent commit are, you will notice that\njj highlights the minimal amount of characters needed to reference this\nchange:</li>\n</ol>\n<pre><code class=\"language-bash\">jj st\nWorking copy changes:\nM README.md\nWorking copy  (@) : mnkrokmt 7f0558f8 say hello and goodbye\nParent commit (@-): ywyvxrts 986d16f5 main | test3\n</code></pre>\n<pre><code class=\"language-bash\">\n</code></pre>\n<p>Being more explicit about your commands ensures both you and jj know where\neverything should go. (i.e. <code>jj desc @ -m</code> explicitly describes <code>@</code>, the working\ncopy.) This will save you some headaches.</p>\n<p>Our new change, the Working copy is now built off of <code>main</code>. The working copy\nwill always be (<code>@</code>).</p>\n<p>Make some changes</p>\n<pre><code class=\"language-bash\">jj st\nWorking copy changes:\nA dev/flake.lock\nA dev/flake.nix\nWorking copy  (@) : kxwrsmmu 42b011cd Add a devShell\nParent commit (@-): ywyvxrts 986d16f5 main | test3\n</code></pre>\n<p>Now I’m done, and since we built this change on top of <code>main</code> the following\ncommand will tell jj we know what we want to push:</p>\n<pre><code class=\"language-bash\">jj bookmark set main\njj git push\n</code></pre>\n<p>If you forget to move a bookmark, JJ will warn you and nothing will be pushed.\nThis is a safety feature, not a bug. That’s what the <code>mb</code> alias does, moves the\nbookmark to the working copy.</p>\n<pre><code class=\"language-nix\"># home-manager alias (move bookmark)\nmb = [\"bookmark\" \"set\" \"-r\" \"@\"];\n</code></pre>\n<p>If you really have problems, <code>jj git push --change @</code> explicitly pushes the\nworking copy.</p>\n<p>This is a bit different than Git and takes some getting used to but you don’t\nneed to move the bookmark after every commit, just when you want to push. I know\nI’ve made the mistake of pushing to the wrong branch before this should prevent\nthat.</p>\n<h2>Here’s an example of using JJ in an existing Git repo</h2>\n<p>Say I have my configuration flake in the <code>~/flakes/</code> directory that is an\nexisting Git repository. To use JJ as the front-end I could do something like:</p>\n<pre><code class=\"language-bash\">cd ~/flakes\njj git init --colocate\nDone importing changes from the underlying Git repo.\nSetting the revset alias `trunk()` to `main@origin`\nInitialized repo in \".\"\n</code></pre>\n<ul>\n<li>By default, JJ defines <code>trunk()</code> as the main development branch of your remote\nrepository. This is usually set to <code>main@origin</code>, but could be named something\nelse. This means that whenever you use <code>trunk()</code> in JJ commands, it will\nresolve to the latest commit on <code>main@origin</code>. This makes it easier to refer\nto the main branch in scripts and commands without hardcoding the branch name.</li>\n</ul>\n<p><strong>Bookmarks</strong> in jj are named pointers to specific revisions, similar to\nbranches in Git. When you first run <code>jj git init --colocate</code> in a git repo, you\nwill likely get a Hint saying “Run the following command to keep local bookmarks\nupdated on future pulls”.:</p>\n<pre><code class=\"language-bash\">jj bookmark list\ntrack main@origin\n\njj st\nThe working copy has no changes.\nWorking copy  (@) : qzxomtxq 925eca75 (empty) (no description set)\nParent commit (@-): qnpnrklz bf291074 main | notes\n</code></pre>\n<p>This shows that running <code>jj git init --colocate</code> automatically started tracking\n<code>main</code> in this case. If it doesn’t, use <code>jj bookmark track main@origin</code>.</p>\n<p>I’ll create a simple change in the <code>README.md</code>:</p>\n<pre><code class=\"language-bash\">jj st\nWorking copy changes:\nM README.md\nWorking copy  (@) : qzxomtxq b963dff0 (no description set)\nParent commit (@-): qnpnrklz bf291074 main | notes\n</code></pre>\n<p>We can see that the working copy now contains a modified file <code>M README.md</code> and\nhas no description set. Lets give it a description before pushing to github.</p>\n<pre><code class=\"language-bash\">jj desc @ -m \"Added to README\"\njj bookmark set main -r @\nMoved 1 bookmarks to pxwnopqo 1e6e08a2 main* | Added to README\n</code></pre>\n<p><code>jj bookmark set main -r @</code> moves the <code>main</code> bookmark to the current revision\n(the working copy), which is the explicit, recommended way to update bookmarks\nin JJ. Without this step, your bookmark will continue to point at the old\ncommit, not your latest work. This is a major difference from Git.</p>\n<p>And finally push to GitHub:</p>\n<pre><code class=\"language-bash\">jj git push\nChanges to push to origin:\n  Move forward bookmark main from bf291074125e to e2a75e45237b\nremote: Resolving deltas: 100% (1/1), completed with 1 local object.\nWarning: The working-copy commit in workspace 'default' became immutable, so a new commit has been created on top of it.\nWorking copy  (@) now at: pxwnopqo 8311444b (empty) (no description set)\nParent commit (@-)      : qzxomtxq e2a75e45 main | Added to README\n</code></pre>\n<hr />\n<h2>Create a Repo without an existing Git Repo</h2>\n<p><strong>Or</strong> to do this in a directory that isn’t already a git repo you can do\nsomething like:</p>\n<pre><code class=\"language-bash\">cargo new hello-world --vcs=none\ncd hello-world\njj git init\nInitialized repo in \".\"\n</code></pre>\n<hr />\n<h3>JJ and Git Side by Side</h3>\n<p>Or for example, with Git if you wanted to move to a different branch before\nrunning <code>nix flake update</code> to see if it introduced errors before merging with\nyour main branch, you could do something like:</p>\n<pre><code class=\"language-bash\">git checkout -b update-test\n\nnix flake update\n\nsudo nixos-rebuild test --flake .\n</code></pre>\n<p>If you’re satisfied you can merge:</p>\n<pre><code class=\"language-bash\">git checkout main\ngit add . # Stage the change\ngit commit -m \"update\"\ngit merge update-test\ngit branch -D update-test\nsudo nixos-rebuild switch --flake .\n</code></pre>\n<p>With JJ a similar workflow could be:</p>\n<ol>\n<li>Run <code>jj st</code> to see what you have:</li>\n</ol>\n<pre><code class=\"language-bash\">jj st\nThe working copy has no changes.\nWorking copy  (@) : ttkstzzn 3f55c42c (empty) (no description set)\nParent commit (@-): wppknozq e3558ef5 main@origin | jj diff\n</code></pre>\n<p>If you don’t have a description set for the working copy set it now.</p>\n<pre><code class=\"language-bash\">jj desc @ -m \"enable vim\"\njj st\nThe working copy has no changes.\nWorking copy  (@) : ttkstzzn 63fda123 (empty) enable vim\nParent commit (@-): wppknozq e3558ef5 main@origin | jj diff\n</code></pre>\n<ol start=\"2\">\n<li>Start from the working copy (which is mutable). The working copy in JJ is\nitself a commit that you can edit and squash changes into. Since <code>main</code> is\nimmutable, you can create your new change by working on top of the working\ncopy commit.</li>\n</ol>\n<p>Create a new change off of the working copy:</p>\n<pre><code class=\"language-bash\">jj new @\n</code></pre>\n<ol start=\"3\">\n<li>Make your edits:</li>\n</ol>\n<pre><code class=\"language-bash\">jj st\nWorking copy changes:\nM home/editors/vim.nix\nWorking copy  (@) : qrsxltmt 494b5f18 (no description set)\nParent commit (@-): wytnnnto a07e775c (empty) enable vim\n</code></pre>\n<ol start=\"4\">\n<li>Squash your changes into the new change:</li>\n</ol>\n<pre><code class=\"language-bash\">jj squash\nThe working copy has no changes.\nWorking copy  (@) : tmlwppnu ba06bb99 (empty) (no description set)\nParent commit (@-): wytnnnto 52928ed9 enable vim\n</code></pre>\n<p>This moves your working copy changes into the new commit you just created.</p>\n<ol start=\"5\">\n<li>Describe the new change, this might feel weird but the <code>jj squash</code> command\ncreated a new commit that you have to describe again:</li>\n</ol>\n<pre><code class=\"language-bash\">jj desc @ -m \"Enabled Vim\"\nWorking copy  (@) : tmlwppnu 5c1569c3 (empty) Enabled Vim\nParent commit (@-): wytnnnto 52928ed9 enable vim\n</code></pre>\n<ol start=\"6\">\n<li>Set the bookmark to the Parent commit that was squashed into:</li>\n</ol>\n<pre><code class=\"language-bash\">jj bookmark set wyt\n</code></pre>\n<ol start=\"7\">\n<li>Finally Push to the remote repository:</li>\n</ol>\n<pre><code class=\"language-bash\">jj git push --allow-new\nChanges to push to origin:\n  Add bookmark wyt to 5c1569c35b22\nremote: Resolving deltas: 100% (4/4), completed with 4 local objects.\nremote:\nremote: Create a pull request for 'wyt' on GitHub by visiting:\nremote:      https://github.com/sayls8/flake/pull/new/wyt\nremote:\n</code></pre>\n<p>This command does the following:</p>\n<ul>\n<li>\n<p>Uploads your bookmark and the associated commit to the remote repository\n(e.g., GitHub).</p>\n</li>\n<li>\n<p>If the bookmark is new (not present on the remote), <code>--allow-new</code> tells JJ\nit’s okay to create it remotely.</p>\n</li>\n<li>\n<p>After pushing, GitHub (or your code host) will usually suggest creating a pull\nrequest for your new branch/bookmark, allowing you or your collaborators to\nreview and merge the change into main.</p>\n</li>\n</ul>\n<p><strong>Merging your Change into <code>main</code></strong></p>\n<p>Option 1. Go to the URL suggested in the output, visit in this case:</p>\n<pre><code class=\"language-bash\">https://github.com/sayls8/flake/pull/new/wyt\n</code></pre>\n<ul>\n<li>\n<p>Click Create PR</p>\n</li>\n<li>\n<p>Click Merge PR if it shows it can merge cleanly.</p>\n</li>\n</ul>\n<p>Option 2.</p>\n<ol>\n<li>Switch to <code>main</code> (if not already there):</li>\n</ol>\n<pre><code class=\"language-bash\">jj bookmark set main\n</code></pre>\n<ol start=\"2\">\n<li>Create a new change that combines the new change with <code>main</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">jj new tml wyt -m \"Merge: enable vim\"\n</code></pre>\n<p>This creates a new commit with both <code>tml</code> and <code>wyt</code> as parents, which is how JJ\nhandles merges (since <code>jj merge</code> depreciated). JJ merges are additive and\nhistory-preserving by design especially for folks used to Git’s fast-forward and\nsquash options.</p>\n<hr />\n<h3>Summary</h3>\n<ul>\n<li>\n<p>With <code>jj</code> you’re creating a new commit rather than a new branch.</p>\n</li>\n<li>\n<p>Amending vs. Squashing: Git’s <code>git commit --amend</code> updates the last commit.\n<code>jj squash</code> combines the current commit with its parent, effectively doing the\nsame thing in terms of history.</p>\n</li>\n<li>\n<p>Merging: Git’s merge command is explicit. In <code>jj</code>, the concept is similar, but\nsince there’s no branch, you’re “merging” by moving your working commit to\ninclude these changes.</p>\n</li>\n<li>\n<p>No need to delete branches: Since there are no branches in <code>jj</code>, there’s no\nequivalent to <code>git branch -D</code> to clean up. Instead commits that are no longer\nneeded can be “abandoned” with <code>jj abandon</code> if you want to clean up your\ncommit graph.</p>\n</li>\n<li>\n<p><code>jj describe</code> without a flag just opens <code>$EDITOR</code> where you can write your\ncommit message save and exit.</p>\n</li>\n<li>\n<p>In <code>git</code>, we finish a set of changes to our code by committing, but in <code>jj</code> we\nstart new work by creating a change, and <em>then</em> make changes to our code. It’s\nmore useful to write an initial description of your intended changes, and then\nrefine it as you work, than it is creating a commit message after the fact.</p>\n</li>\n<li>\n<p>I have heard that jj can struggle with big repositories such as Nixpkgs and\nhave noticed some issues here and there when using with NixOS. I’m hoping that\nas the project matures, it gets better on this front.</p>\n</li>\n</ul>\n<hr />\n<h2>The 2 main JJ Workflows</h2>\n<h3>The Squash Workflow</h3>\n<p>This workflow is the most similar to Git and Git’s index.</p>\n<p>The workflow:</p>\n<ol>\n<li>\n<p>Describe the work we want to do with <code>jj desc -m \"message\"</code></p>\n</li>\n<li>\n<p>We create a new empty change on top of that one with <code>jj new</code></p>\n</li>\n<li>\n<p>When we are done with a feature, we run <code>jj squash</code> to move the changes from\n<code>@</code> into the change we described in step 1. <code>@</code> is where your working copy is\npositioned currently.</p>\n</li>\n</ol>\n<p>For example, let’s say we just ran <code>jj git init --colocate</code> in our configuration\nFlake directory making it a <code>jj</code> repo as well using git for backend.</p>\n<pre><code class=\"language-bash\">cd flake\njj git init --colocate\njj log\n@  lnmmxwko sayls8@proton.me 2025-06-27 10:14:57 1eac6aa0\n│  (empty) (no description set)\n○  qnknltto sayls8@proton.me 2025-06-27 09:04:08 git_head() 5358483a\n│  (empty) jj\n</code></pre>\n<p>The above log output shows that running <code>jj git init</code> creates an empty working\ncommit (<code>@</code>) on top of the <code>git_head()</code></p>\n<pre><code class=\"language-bash\">jj desc -m \"Switch from nixVim to NVF\"\njj new  # Create a new empty change\njj log\n@  nmnmznmm sayls8@proton.me 2025-06-27 10:16:30 52dd7ee0\n│  (empty) (no description set)\n○  lnmmxwko sayls8@proton.me 2025-06-27 10:16:24 git_head() 3e8f9f3a\n│  (empty) Switch from nixVim to NVF\n○  qnknltto sayls8@proton.me 2025-06-27 09:04:08 5358483a\n│  (empty) jj\n</code></pre>\n<p>The above log shows that running <code>jj desc</code> changes the current (<code>@</code>) commits\ndescription, and then <code>jj new</code> creates a new empty commit on top of it, moving\n(<code>@</code>) to this new empty commit.</p>\n<p>The “Switch from nixVim to NVF” commit is now the parent of (<code>@</code>).</p>\n<p>Now, we’d make the necessary changes and to add them to the commit we just\ndescribed in the previous steps.</p>\n<p>The changes are automatically “staged” so theres no need to <code>git add</code> them, so\nwe just make the changes and squash them.</p>\n<pre><code class=\"language-bash\">jj squash  # Squash the commit into its parent commit (i.e., our named commit)\njj log\n@  zsxsolsq sayls8@proton.me 2025-06-27 10:18:01 2c35d83f\n│  (empty) (no description set)\n○  lnmmxwko sayls8@proton.me 2025-06-27 10:18:01 git_head() 485eaee9\n│  (empty) Switch from nixVim to NVF\n</code></pre>\n<p>This shows <code>jj squashes</code> effect, it merges the changes from the current (<code>@</code>)\ncommit into its parent. The (<code>@</code>) then moves to this modified parent, and a new\nempty commit is created on top, ready for the next set of changes.</p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --flake .\n</code></pre>\n<p>We’re still in the nameless commit and can either continue working or run\n<code>jj desc -m \"\"</code> again describing our new change, then <code>jj new</code> and <code>jj squash</code>\nit’s pretty simple. The nameless commit is used as an adhoc staging area.</p>\n<p>When you are ready to push, it’s important to know where your working copy\ncurrently is and if it’s attached to a bookmark. It’s common for <code>jj new</code> to\ndetach the head, all you have to do is tell JJ which branch to attach to, then\npush:</p>\n<pre><code class=\"language-bash\">jj st\nWorking copy changes:\nM hosts/magic/configuration.nix\nM hosts/magic/container.nix\nWorking copy  (@) : youptvvn 988e6fc9 (no description set)\nParent commit (@-): qlwqromx 4bb754fa mdbook container\n</code></pre>\n<p>The above output means that the working copy has modifications (<code>M</code>) in two\nfiles. And these changes are not yet committed.</p>\n<pre><code class=\"language-bash\">jj bookmark set main\njj git push\n</code></pre>\n<hr />\n<h3>The Edit Workflow</h3>\n<p>This workflow adds a few new commands <code>jj edit</code>, and <code>jj next</code>.</p>\n<p>Here’s the workflow:</p>\n<ol>\n<li>\n<p>Create a new change to work on the new feature with <code>jj new</code></p>\n</li>\n<li>\n<p>If everything works exactly as planned, we’re done.</p>\n</li>\n<li>\n<p>If we realize we want to break this big change up into multiple smaller ones,\nwe do it by making a new change before the current one, swapping to it, and\nmaking the necessary change.</p>\n</li>\n<li>\n<p>Lastly, we go back to the main change.</p>\n</li>\n</ol>\n<p>The squash workflow leaves <code>@</code> at an empty undescribed change, with this\nworkflow, <code>@</code> will often be on the existing change.</p>\n<p>If <code>@</code> wasn’t at an empty change, we would start this workflow with:</p>\n<pre><code class=\"language-bash\">jj new -m \"Switch from NVF to nixVim\"\n</code></pre>\n<p>since our <code>@</code> is already at an empty change, we’ll just describe it and get\nstarted:</p>\n<p>For this example, lets say we want to revert back to nixVim:</p>\n<pre><code class=\"language-bash\">jj desc -m \"Switch from NVF to nixVim\"\njj log\n@  zsxsolsq sayls8@proton.me 2025-06-27 10:18:47 606abaa7\n│  (empty) Switch from NVF to nixVim\n○  lnmmxwko sayls8@proton.me 2025-06-27 10:18:01 git_head() 485eaee9\n│  (empty) Switch from nixVim to NVF\n○  qnknltto sayls8@proton.me 2025-06-27 09:04:08 5358483a\n│  (empty) jj\n</code></pre>\n<p>Again, this shows <code>jj desc</code> renaming the current empty <code>@</code> commit.</p>\n<p>We make the changes, and it’s pretty straightforward so we’re done, every change\nis automatically staged so we can just run <code>sudo nixos-rebuild switch --flake .</code>\nnow to apply the changes.</p>\n<p>If we wanted to make more changes that aren’t described we can use <code>jj new -B</code>\nwhich is similar to <code>git add -a</code>.</p>\n<pre><code class=\"language-bash\">jj new -B @ -m \"Adding LSP to nixVim\"\nRebased 1 descendant commits\nWorking copy  (@) now at: lpnxxxpo bf929946 (empty) Adding LSP to nixVim\nParent commit (@-)      : lnmmxwko 485eaee9 (empty) Switch from nixVim to NVF\n</code></pre>\n<p>The <code>-B</code> tells jj to create the new change <em>before</em> the current one and it\ncreates a rebase. We created a change before the one we’re on, it automatically\nrebased our original change. This operation will <em>always</em> succeed with jj, we\nwill have our working copy at the commit we’ve just inserted.</p>\n<p>You can see below that <code>@</code> moved down one commit:</p>\n<pre><code class=\"language-bash\">jj log\n○  zsxsolsq sayls8@proton.me 2025-06-27 10:22:03 ad0713b6\n│  (empty) Switch from NVF to nixVim\n@  lpnxxxpo sayls8@proton.me 2025-06-27 10:22:03 bf929946\n│  (empty) Adding LSP to nixVim\n○  lnmmxwko sayls8@proton.me 2025-06-27 10:18:01 git_head() 485eaee9\n│  (empty) Switch from nixVim to NVF\n○  qnknltto sayls8@proton.me 2025-06-27 09:04:08 5358483a\n│  (empty) jj\n○  qnknltto sayls8@proton.me 2025-06-27 09:04:08 git_head()\n</code></pre>\n<p>The “Adding LSP to nixVim” commit is directly above “Switch from nixVim to NVF”\n(the old <code>git_head()</code>)</p>\n<p>The “Switch from NVF to nixVim” commit (which was your <code>@</code> before <code>jj new -B</code>)\nis now above “Adding LSP to nixVim” in the log output, meaning “Adding LSP to\nnixVim” is its new parent.</p>\n<p><code>@</code> has moved to “Adding LSP to nixVim”</p>\n<p><code>jj log</code> example output</p>\n<hr />\n<h2>Operation Log and Undo</h2>\n<p>JJ records every operation (commits, merges, rebases, etc.) in an operation log.\nYou can view and undo previous operations, making it easy to recover from\nmistakes, a feature not present in Git’s core CLI</p>\n<pre><code class=\"language-bash\">jj op log\n@  fbf6e626df22 jr@magic 15 minutes ago, lasted 9 milliseconds\n│  new empty commit\n│  args: jj new -B @ -m 'Adding LSP to nixVim'\n○  bde40b7c17cf jr@magic 19 minutes ago, lasted 8 milliseconds\n│  describe commit 2c35d83f75031dc582bf28b64d4af1c218177f90\n│  args: jj desc -m 'Switch from NVF to nixVim'\n○  3a2bfe1c0b0a jr@magic 19 minutes ago, lasted 8 milliseconds\n│  squash commits into 3e8f9f3a6a58fef86906e16e9b4375afb43e73e3\n│  args: jj squash\n○  80abcb58dcb6 jr@magic 21 minutes ago, lasted 8 milliseconds\n│  new empty commit\n│  args: jj new\n○  8c80314cbcd7 jr@magic 21 minutes ago, lasted 8 milliseconds\n│  describe commit 1eac6aa0b88ba014785ee9c1c2ad6e2abc6206e9\n│  args: jj desc -m 'Switch from nixVim to NVF'\n○  44b5789cb4d1 jr@magic 22 minutes ago, lasted 6 milliseconds\n│  track remote bookmark main@origin\n│  args: jj bookmark track main@origin\n○  dbefee04aa85 jr@magic 23 minutes ago, lasted 4 milliseconds\n│  import git head\n│  args: jj git init --git-repo .\n</code></pre>\n<pre><code class=\"language-bash\">jj op undo &lt;operation-id&gt;\n# or\njj op restore &lt;operation-id&gt;\n</code></pre>\n<hr />\n<h2>Conflict Resolution</h2>\n<p>In JJ, conflicts live inside commits and can be resolved at any time, not just\nduring a merge. This makes rebasing and history editing safer and more flexible</p>\n<p>JJ treats conflicts as first-class citizens: conflicts can exist inside commits,\nnot just in the working directory. This means if a merge or rebase introduces a\nconflict, the conflicted state is saved in the commit itself, and you can\nresolve it at any time there’s no need to resolve conflicts immediately or use\n“<code>--continue</code>” commands as in Git</p>\n<p>Here’s how it works:</p>\n<p>When you check out or create a commit with conflicts, JJ materializes the\nconflicts as markers in your files (similar to Git’s conflict markers)</p>\n<p>You can resolve conflicts by editing the files to remove the markers, or by\nusing:</p>\n<pre><code class=\"language-bash\">jj resolve\n</code></pre>\n<hr />\n<h2>Revsets</h2>\n<p><a href=\"https://jj-vcs.github.io/jj/latest/revsets/\">Jujutsu Revsets</a></p>\n<p>JJ includes a powerful query language for selecting commits. For example:</p>\n<pre><code class=\"language-bash\">jj log -r \"author(alice) &amp; file(*.py)\"\n</code></pre>\n<p>This command lists all commits by Alice that touch Python files.</p>\n<h2>Filesets</h2>\n<p><a href=\"https://jj-vcs.github.io/jj/latest/filesets/\">Jujutsu Filesets</a></p>\n<p>Jujutsu supports a functional language for selecting a set of files. Expressions\nin this language are called “filesets” (the idea comes from Mercurial). The\nlanguage consists of file patterns, operators, and functions. –JJ Docs</p>\n<h2>Summary</h2>\n<p>Jujutsu (jj) offers a streamlined, branchless, and undo-friendly approach to\nversion control, fully compatible with Git but designed to be easier to use and\nreason about. Its workflows, operation log, and conflict handling provide a\nsafer and more flexible environment for managing code changes, making it a\ncompelling alternative for both new and experienced developers.</p>\n<hr />\n<h3>Resources</h3>\n<ul>\n<li>\n<p><a href=\"https://steveklabnik.github.io/jujutsu-tutorial/\">steves_jj_tutorial</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/jj-vcs/jj\">jj_github</a></p>\n</li>\n<li>\n<p><a href=\"https://jj-vcs.github.io/jj/latest/tutorial/\">official_tutorial</a></p>\n</li>\n<li>\n<p><a href=\"https://v5.chriskrycho.com/essays/jj-init/\">jj_init</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-12-08T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/kvm.html",
      "url": "https://saylesss88.github.io/nix/kvm.html",
      "title": "KVM",
      "content_html": "<h1>Running NixOS in a VM with Maximum Isolation (Beginner Guide)</h1>\n<details>\n<summary> Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/steampunk5.cleaned.png\" alt=\"sp5\" /></p>\n<h2>Why This Setup?</h2>\n<ul>\n<li>\n<p><strong>Host</strong> <code>secureblue</code> = Fedora Atomic with <strong>SELinux enforcing</strong>, <strong>sVirt</strong>,\n<strong>Secure Boot</strong>, and hardened defaults.</p>\n</li>\n<li>\n<p><strong>Guest</strong>: NixOS in a VM → full declarative power, near zero risk to host.</p>\n</li>\n<li>\n<p><strong>Isolation</strong>: Mandatory Access Control (MAC) via SELinux + KVM + no direct\nhardware access.</p>\n</li>\n</ul>\n<blockquote>\n<p>NOTE: Secureblue enables the <code>hardened_malloc</code> by default which causes\nproblems for many browsers and will cause screen flashing with Firefox and\nothers within the VM. See:</p>\n</blockquote>\n<ul>\n<li><a href=\"https://secureblue.dev/faq#standard-malloc\">secureblue standard_malloc</a></li>\n</ul>\n<h2>Step 1: Install secureblue (Hardened Host)</h2>\n<ol>\n<li>\n<p>Download a <a href=\"https://secureblue.dev/install\">secureblue image</a></p>\n</li>\n<li>\n<p>Use <strong>Fedora Media Writer</strong> (Flatpak):</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">flatpak install flathub org.fedoraproject.MediaWriter\n</code></pre>\n<ol start=\"3\">\n<li>\n<p>Flash the secureblue image &amp; enable Secure Boot in UEFI <strong>before</strong> install.\nThis is now possible with Fedora, when you boot into Fedora Media Writer (not\nVentoy or Rufus), you will be allowed to enroll the secure boot key with\nsecure boot pre-enabled.</p>\n</li>\n<li>\n<p>On first boot:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">ujust enroll-secureblue-secure-boot-key\n</code></pre>\n<ul>\n<li>Reboot -&gt; Enroll key in MOK manager with password: <code>secureblue</code></li>\n</ul>\n<ol start=\"5\">\n<li>\n<p>Post-install hardening See:\n<a href=\"https://secureblue.dev/post-install\">post-install</a></p>\n</li>\n<li>\n<p>Install virtualization stack:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">ujust install-libvirt-packages\n</code></pre>\n<ul>\n<li>\n<p>The above command enables <code>qemu</code>, <code>libvirt</code>, &amp; <code>virt-manager</code> with SELinux\nlabels.</p>\n</li>\n<li>\n<p>Read the <a href=\"https://secureblue.dev/faq\">secureblue FAQ</a> to learn the quirks of\nan atomic fedora image.</p>\n</li>\n</ul>\n<p>Secureblue recommends installing GUI apps with Flatpak, CLI apps with homebrew,\nand apps that require more system access to be layered with <code>rpm-ostree</code>. It\ntakes some getting used to but is very stable.</p>\n<ul>\n<li><a href=\"https://secureblue.dev/faq#software\">secureblue how to install software</a></li>\n</ul>\n<hr />\n<h2>Create NixOS VM (via virt-manager)</h2>\n<p>Easiest way to get a working configuration IMO:</p>\n<ol>\n<li>\n<p>Download: <a href=\"https://nixos.org/download/\">NixOS Graphical ISO</a></p>\n</li>\n<li>\n<p>Open <code>virt-manager</code> -&gt; File -&gt; New Virtual Machine</p>\n</li>\n</ol>\n<ul>\n<li>\n<p>Select ISO</p>\n</li>\n<li>\n<p>CPU: <code>host-passthrough</code> (optional, for performance)</p>\n</li>\n<li>\n<p>Do some research to find the ideal Memory and Storage for your system.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>\n<p>Ensure SELinux is enabled (the default for secureblue) with: <code>getenforce</code></p>\n</li>\n<li>\n<p>Ensure sVirt is enabled (the default) with <code>run0 ps -eZ | grep qemu</code>.</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">run0 ps -eZ | grep qemu\n# Output\nsystem_u:system_r:svirt_t:s0:c383,c416 14793 ?   00:01:37 qemu-system-x86\n</code></pre>\n<ol start=\"5\">\n<li>Boot -&gt; Follow graphical installer:</li>\n</ol>\n<ul>\n<li>\n<p>Enable LUKS</p>\n</li>\n<li>\n<p>Create an admin user</p>\n</li>\n<li>\n<p>Optionally skip desktop -&gt; install your own after first boot.</p>\n</li>\n</ul>\n<p>The attack surface is reduced significantly when running NixOS within a hardened\nhosts VM. The VM operates on virtualized hardware, which is a powerful form of\nattack surface reduction.</p>\n<p>Devices like your host’s Bluetooth adapter, Wi-Fi card, microphone, webcam, and\nUSB ports are not directly exposed to the guest operating system. The VM only\nsees virtual versions of these devices. If an exploit targets a vulnerability in\nthe Bluetooth stack within the VM, it compromises the VM environment, but it\ncannot typically reach and exploit the physical Bluetooth hardware on the host.</p>\n<p>You can also choose not to pass through certain devices, like Bluetooth or\nwebcam to the VM at all, effectively disabling that attack vector. Since your\nhost likely already has these hardened features you may not need the additional\nfunctionality within the VM.</p>\n<p>If something breaks, you have an option to rollback to the previous generation\nwith <code>rpm-ostree rollback</code>. The previous generation will be applied on next\nreboot. You can also just reboot and choose the previous generation through the\ngrub menu, this way it is temporary and will revert back on next reboot.</p>\n<hr />\n<h2>🔒 How Host MAC Secures the NixOS VM</h2>\n<p>The host uses a classic defense-in-depth model: the hardened outer layer (the\nhost) is treated as the real security boundary, and it is designed to remain\nsafe even if the inner layer (the NixOS guest) is fully compromised.</p>\n<ol>\n<li><strong>MAC confinement with SELinux and sVirt</strong></li>\n</ol>\n<p>On the secureblue host, sVirt automatically applies SELinux labels to all\nVM-related processes and resources.</p>\n<ul>\n<li>\n<p><strong>QEMU process confinement</strong>: The QEMU process that runs the NixOS VM runs\nunder a dedicated SELinux type, typically <code>svirt_t</code>. The host’s MAC policy\ntightly restricts what this process can access, so even a successful VM escape\nis still trapped inside a very limited sandbox rather than gaining normal host\nprivileges.</p>\n</li>\n<li>\n<p><strong>Disk image protection</strong>: VM disk images are labeled (for example,\n<code>virt_image_t</code>), which prevents unrelated host processes from reading or\nmodifying them and keeps the VM’s storage isolated from the rest of the\nsystem.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li><strong>KVM and host hardening</strong></li>\n</ol>\n<p>KVM provides the hardware-assisted virtualization layer and forms a strong\nbarrier between the guest and the host kernel. On top of that, the secureblue\nhost is hardened with SELinux in enforcing mode, Secure Boot, a hardened kernel,\nand hardened_malloc by default. Together, these measures reduce the attack\nsurface and help ensure the integrity of the platform that is actually running\nthe VM.</p>\n<ol start=\"3\">\n<li><strong>Isolation and “zero host compromise”</strong></li>\n</ol>\n<p>The host and guest are deliberately decoupled from a security perspective. The\nassumption is that the NixOS VM can be misconfigured, vulnerable, or even fully\ncompromised. If that happens, KVM plus the host MAC policy (SELinux + sVirt) are\nresponsible for containing the damage. In other words, the security boundary is\nnot the NixOS configuration inside the VM, but the hypervisor and the host’s\nmandatory access control rules that enforce strict isolation of the guest from\nthe host.</p>\n<h2>Hardening the NixOS Guest is Still Worth It</h2>\n<p>Even with a hardened host and MAC confinement, treating the NixOS VM as\n“untrusted but hardened” adds another independent safety layer. The goal is to\nminimize what an attacker can do inside the guest, even if they never manage a\nbreakout.</p>\n<p><strong>Minimize VM device exposure</strong></p>\n<ul>\n<li>\n<p><strong>Use snapshots aggressively</strong>: Take a snapshot right after a fresh install\nand initial configuration. That snapshot becomes your “known-good” state for\ntesting risky software or malware, so you can revert and wipe out any changes\nafterward.</p>\n</li>\n<li>\n<p><strong>Avoid unnecessary passthrough</strong>: Only pass through hardware (USB, GPU,\nnetwork interfaces, etc.) if the VM genuinely needs it. Every extra device is\nanother potential attack surface.</p>\n</li>\n<li>\n<p><strong>Prefer simple virtual devices</strong>: Use virtio and other paravirtualized\ndevices where possible, and avoid legacy or fully emulated devices unless\nthere is a specific need.</p>\n</li>\n</ul>\n<p><strong>Network isolation for guests</strong></p>\n<ul>\n<li>\n<p><strong>Keep networks virtual and segmented</strong>: Favor isolated virtual networks,\nVLANs, or internal-only networks over bridged physical interfaces, so VMs\ncannot talk to the host or each other unless you explicitly design for it.</p>\n</li>\n<li>\n<p><strong>Filter traffic tightly</strong>: Use libvirt nwfilter, firewall rules\n(nftables/iptables/firewalld), and similar tools to restrict VM-to-VM and\nVM-to-external traffic, especially for services exposed on multiple guests.</p>\n</li>\n<li>\n<p><strong>Be cautious with IPv6</strong>: Full IPv6 inside the VM usually implies bridged\nnetworking, which connects the VM more directly to the host’s LAN. That\nimproves connectivity but reduces isolation, so enable it only if you truly\nneed it.</p>\n</li>\n</ul>\n<p><strong>Guest-side hardening measures</strong></p>\n<ul>\n<li><strong>Harden the allocator</strong>: Enabling <code>graphene-hardened</code> or\n<code>graphene-hardened-light</code> inside the guest improves memory safety for many\napplications:</li>\n</ul>\n<pre><code class=\"language-nix\"># configuration.nix\nenvironment.memoryAllocator.provider = \"graphene-hardened\";\n# OR for a more permissive and better performing allocator:\n# environment.memoryAllocator.provider = \"graphene-hardened-light\";\n</code></pre>\n<p>Some software (notably certain browsers) can be finicky with hardened mallocs\nand may require rebuilding or alternatives; be prepared to switch to another\nbrowser or allocator profile when you hit incompatibilities.</p>\n<ul>\n<li><strong>Disable nonessential features</strong>: Turn off USB redirection/debugging, audio\ndevices, and other “extras” you do not need in the VM. These often pull in\ncomplex subsystems that are rarely worth the extra attack surface for a\nsecurity-focused guest.</li>\n</ul>\n<p>For deeper NixOS-specific hardening, see:\n<a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html\">hardening NixOS</a></p>\n<hr />\n<h3>Nix Toolbox</h3>\n<blockquote>\n<p>⚠️ Warning: toolbx containers are integrated with the host system, don’t do\nthings you wouldn’t do on your host. Toolbx containers are not fully isolated\nenvironments like VMs.</p>\n</blockquote>\n<p>That said, they are a fast and convenient way to spin up a Nix development\nenvironment. Know the limitations and benefits, and when you need more\nisolation, just spin up a VM instead.</p>\n<p>Secureblue enforces restrictive container image policies by default, blocking\nunsigned or unverified images from registries like GitHub Container Registry.\nThis requires explicit trust configuration for each container source.</p>\n<pre><code class=\"language-bash\">ujust set-container-userns on\n</code></pre>\n<p>Without this setting, containers will fail with <code>OCI permission denied</code> errors.</p>\n<p><strong>Allow the Nix-Toolbox Image</strong></p>\n<pre><code class=\"language-bash\"># For system-wide configuration (affects all users)\nrun0 podman image trust set -t accept ghcr.io/thrix/nix-toolbox\n\n# For user-specific configuration (recommended for development)\npodman image trust set -t accept ghcr.io/thrix/nix-toolbox\n</code></pre>\n<p>The <code>-t accept</code> flag allows images from this registry without requiring\nsignature verification.</p>\n<pre><code class=\"language-bash\"># Check that the policy has been updated correctly:\npodman image trust show\n</code></pre>\n<p>Create the <code>nix-toolbox</code> container:</p>\n<pre><code class=\"language-bash\">toolbox create --image ghcr.io/thrix/nix-toolbox:42\n</code></pre>\n<p>You will be prompted whether you want Home Manager installed or not as well.</p>\n<p>Enter the toolbox:</p>\n<pre><code class=\"language-bash\">toolbox enter nix-toolbox-42\n</code></pre>\n<p>You can then use nix and home-manager to setup a fully declarative\ndev-environment.</p>\n<hr />\n<h3>Real-world recovery example</h3>\n<p>Secureblue’s design and the underlying firmware safeguards also make certain\nfailures recoverable. On a mini PC, running a firmware update command resulted\nin a boot error (“Something went seriously wrong, MOK is full”) and a forced\nshutdown. Resetting NVRAM by moving the jumper on the motherboard briefly, then\nrestoring it to the original position, allowed the system to retrain and boot\nagain, after which the Secure Boot key could be re-enrolled and the system\nreturned to a known-good, secure state.</p>\n<details>\n<summary> ✔️ My Experience with Secureblue </summary>\n<p>Using secureblue as the host OS with NixOS in a VM has been surprisingly smooth\nfor day‑to‑day work. Performance has been more than adequate for editing,\ndevelopment, and browsing, and every issue so far has been fixable with\nrollbacks or small config changes—no “nuke and reinstall” moments required.</p>\n<p><strong>Software installation and workflows</strong></p>\n<p>Flatpak takes a bit of relearning if you are used to installing everything with\nfull root on a mutable distro. Tools like Flatseal help a lot: you can see\nexactly which permissions an app has, then selectively tighten them instead of\nblindly trusting defaults. On secureblue, running the\n<code>ujust flatpak-permissions-lockdown</code> helper gives you a very strict baseline,\nthen you add back only what each app truly needs.</p>\n<p>In practice, a hybrid approach has worked best. One editor runs as a Flatpak,\nand another is installed via <code>rpm-ostree</code> for tighter system integration and the\n“traditional” root behavior when needed. The same thing happened with <code>yazi</code>: to\nget the exact workflow wanted, it was easier to install it via <code>rpm-ostree</code>\nrather than poking so many holes in the Flatpak sandbox that most isolation\nbenefits disappeared.</p>\n<p>Toolbx also fits into this nicely. Putting Homebrew and Flatpak inside a toolbox\nlets more config be shared while keeping the host image clean. The general\npattern on Silverblue/secureblue is: Flatpak for most GUI apps, toolbox (plus\nbrew or distro packages) for CLI tooling, and only a small number of host‑layer\n<code>rpm-ostree</code> installs when deep integration is really warranted.</p>\n<p>One quirk worth knowing about: on secureblue, <code>/home</code> is a symlink to\n<code>/var/home</code>. Most tools don’t care, but a few development workflows get confused\nby the indirection. In those cases, pointing the tool directly at\n<code>/var/home/username</code> instead of <code>/home/username</code> usually clears things up.</p>\n<p><strong>Graphics and drivers in the NixOS VM</strong></p>\n<p>For GPU and display, the safest approach has been to let secureblue own the\nhardware stack and keep the NixOS guest as simple as possible. Extra GPU drivers\nor compositor tweaks inside the VM tended to make things less stable, showing up\nas flicker, random freezes, or generally janky graphics because the guest was\neffectively fighting the host’s configuration. Sticking close to the defaults in\nthe VM has consistently produced smoother and more predictable graphics\nbehavior.</p>\n<p>The main issue I’ve had is with the dns-selector occasionally causing networking\nproblems. I configure global DNS with their <code>ujust</code> command and I’m assuming\nthat updates were incompatible with my DNS setup. Running <code>ujust dns-selector</code>\nand pressing <code>1</code> (Reset to defaults), and a reboot typically fix the connection\nand within a few days the global DNS will work again.</p>\n</details>\n<hr />\n<h3>Resources</h3>\n<ul>\n<li>\n<p><a href=\"https://www.redhat.com/en/topics/virtualization/what-is-virtualization\">RedHat What is virtualization?</a></p>\n</li>\n<li>\n<p><a href=\"https://sumit-ghosh.com/posts/virtualization-hypervisors-explaining-qemu-kvm-libvirt/\">virtualization &amp; hypervisors</a></p>\n</li>\n<li>\n<p><a href=\"https://bitgrounds.tech/posts/kvm-qemu-libvirt-virtualization/\">Virtualization on Linux using the KVM/QEMU/Libvirt stack</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-12-06T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/helix_flake_4.4.html",
      "url": "https://saylesss88.github.io/flakes/helix_flake_4.4.html",
      "title": "Understanding the Helix Flake",
      "content_html": "<h1>Chapter 4.4</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/../images/helix.png\" alt=\"Helix Logo\" />–<a href=\"https://helix-editor.com/\">helix-editor.com</a></p>\n<h1>Understanding the Helix Flake and Modifying its Behavior</h1>\n<p>As we’ve seen from previous examples, the helix editor repository includes a few\n<code>.nix</code> files including a <code>flake.nix</code>. Their flake uses a lot of idiomatic Nix\ncode and advanced features. First I will break down their <code>flake.nix</code> and\n<code>default.nix</code> to understand why they do certain things. And finally, we will\nchange the build to “debug” mode demonstrating how easily you can modify the\nbehavior of a package defined within a Nix flake without changing the original\nsource code or the upstream flake directly.</p>\n<ol>\n<li>Let’s clone the Helix repository:</li>\n</ol>\n<pre><code class=\"language-bash\">git clone https://github.com/helix-editor/helix.git\ncd helix\n</code></pre>\n<p>When you enter the <code>helix</code> directory, <code>direnv</code> is setup for you already. All you\nwould have to do is <code>direnv allow</code> and it will ask you a few questions then you\nare good to go. Looking at their <code>.envrc</code> it mentions “try to use flakes, if it\nfails use normal nix (i.e., shell.nix)”. If it’s successful you’ll see a long\nlist of environment variables displayed.</p>\n<ol start=\"2\">\n<li>Enter the Development Shell:</li>\n</ol>\n<p>The Helix project’s <code>flake.nix</code> includes a <code>devShells.default</code> output,\nspecifically designed for development.</p>\n<pre><code class=\"language-bash\">nix develop\n</code></pre>\n<ol start=\"3\">\n<li>You’re now in a fully configured development environment:</li>\n</ol>\n<ul>\n<li>When you run <code>nix develop</code>, Nix builds and drops you into a shell environment\nwith all the dependencies specified in <code>devShells.default</code>. This means you\ndon’t have to manually install or manage tools like Rust, Cargo, or Clang,\nit’s all handled declaratively through Nix.</li>\n</ul>\n<p>You can now build and run the project using its standard tooling:</p>\n<pre><code class=\"language-bash\">cargo check\ncargo build\ncargo run\n</code></pre>\n<ol start=\"4\">\n<li>Making Changes and Testing Them</li>\n</ol>\n<p>Since you’re in a reproducible environment, you can confidently hack on the\nproject without worrying about your system setup. Try modifying some code in\n<code>helix</code> and rebuilding with Cargo. The Nix shell ensures consistency for every\ncontributor or device you work on.</p>\n<ol start=\"5\">\n<li>Run Just the Binary</li>\n</ol>\n<p>If you only want to run the compiled program without entering the shell, use the\nnix run command:</p>\n<pre><code class=\"language-bash\">nix run\n</code></pre>\n<p>This builds and runs the default package defined by the flake. In the case of\nHelix, this launches the <code>hx</code> editor directly.</p>\n<ol start=\"6\">\n<li>Build Without Running</li>\n</ol>\n<p>To just build the project and get the path to the output binary:</p>\n<pre><code class=\"language-bash\">nix build\n</code></pre>\n<p>You’ll find the compiled binary under <code>./result/bin</code>.</p>\n<ol start=\"7\">\n<li>Pinning and Reproducing</li>\n</ol>\n<p>Because the project uses a flake, you can ensure full reproducibility by pinning\nthe inputs. For example, you can clone with <code>--recurse-submodules</code> and copy the\n<code>flake.lock</code> to ensure you’re using the same dependency versions as upstream.\nThis is great for debugging or sharing exact builds.</p>\n<p>✅ Recap:</p>\n<p>With flakes, projects like Helix provide everything you need for development and\nrunning in a single <code>flake.nix</code>. You can nix develop to get started hacking, nix\nrun to quickly try it out, and nix build to produce binaries all without\ninstalling or polluting your system.</p>\n<h2>Understanding the Helix flake.nix</h2>\n<p>The helix flake is full of idiomatic Nix code and displays some of the more\nadvanced things a flake can provide:</p>\n<pre><code class=\"language-nix\">{\n  description = \"A post-modern text editor.\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    rust-overlay = {\n      url = \"github:oxalica/rust-overlay\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n    rust-overlay,\n    ...\n  }: let\n    inherit (nixpkgs) lib;\n    systems = [\n      \"x86_64-linux\"\n      \"aarch64-linux\"\n      \"x86_64-darwin\"\n      \"aarch64-darwin\"\n    ];\n    eachSystem = lib.genAttrs systems;\n    pkgsFor = eachSystem (system:\n      import nixpkgs {\n        localSystem.system = system;\n        overlays = [(import rust-overlay) self.overlays.helix];\n      });\n    gitRev = self.rev or self.dirtyRev or null;\n  in {\n    packages = eachSystem (system: {\n      inherit (pkgsFor.${system}) helix;\n      /*\n      The default Helix build. Uses the latest stable Rust toolchain, and unstable\n      nixpkgs.\n\n      The build inputs can be overridden with the following:\n\n      packages.${system}.default.override { rustPlatform = newPlatform; };\n\n      Overriding a derivation attribute can be done as well:\n\n      packages.${system}.default.overrideAttrs { buildType = \"debug\"; };\n      */\n      default = self.packages.${system}.helix;\n    });\n    checks =\n      lib.mapAttrs (system: pkgs: let\n        # Get Helix's MSRV toolchain to build with by default.\n        msrvToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;\n        msrvPlatform = pkgs.makeRustPlatform {\n          cargo = msrvToolchain;\n          rustc = msrvToolchain;\n        };\n      in {\n        helix = self.packages.${system}.helix.override {\n          rustPlatform = msrvPlatform;\n        };\n      })\n      pkgsFor;\n\n    # Devshell behavior is preserved.\n    devShells =\n      lib.mapAttrs (system: pkgs: {\n        default = let\n          commonRustFlagsEnv = \"-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable\";\n          platformRustFlagsEnv = lib.optionalString pkgs.stdenv.isLinux \"-Clink-arg=-Wl,--no-rosegment\";\n        in\n          pkgs.mkShell {\n            inputsFrom = [self.checks.${system}.helix];\n            nativeBuildInputs = with pkgs;\n              [\n                lld\n                cargo-flamegraph\n                rust-bin.nightly.latest.rust-analyzer\n              ]\n              ++ (lib.optional (stdenv.isx86_64 &amp;&amp; stdenv.isLinux) cargo-tarpaulin)\n              ++ (lib.optional stdenv.isLinux lldb)\n              ++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation);\n            shellHook = ''\n              export RUST_BACKTRACE=\"1\"\n              export RUSTFLAGS=\"''${RUSTFLAGS:-\"\"} ${commonRustFlagsEnv} ${platformRustFlagsEnv}\"\n            '';\n          };\n      })\n      pkgsFor;\n\n    overlays = {\n      helix = final: prev: {\n        helix = final.callPackage ./default.nix {inherit gitRev;};\n      };\n\n      default = self.overlays.helix;\n    };\n  };\n  nixConfig = {\n    extra-substituters = [\"https://helix.cachix.org\"];\n    extra-trusted-public-keys = [\"helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs=\"];\n  };\n}\n</code></pre>\n<p><strong>Top-Level Metadata</strong>:</p>\n<pre><code class=\"language-nix\">{\n  description = \"A post-modern text editor.\";\n}\n</code></pre>\n<ul>\n<li>This sets a human-readable description for the flake.</li>\n</ul>\n<h2>Inputs</h2>\n<pre><code class=\"language-nix\">inputs = {\n  nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n  rust-overlay = {\n    url = \"github:oxalica/rust-overlay\";\n    inputs.nixpkgs.follows = \"nixpkgs\";\n  };\n};\n</code></pre>\n<ul>\n<li>\n<p><code>nixpkgs</code>: Uses the <code>nixos-unstable</code> branch of the Nixpkgs repository.</p>\n</li>\n<li>\n<p><code>rust-overlay</code>: follows the same <code>nixpkgs</code>, ensuring compatibility between\ninputs.</p>\n</li>\n</ul>\n<p><strong>Outputs Function</strong>:</p>\n<pre><code class=\"language-nix\">outputs = { self, nixpkgs, rust-overlay, ... }:\n</code></pre>\n<ul>\n<li>This defines what this flake exports, including <code>packages</code>, <code>devShells</code>, etc.</li>\n</ul>\n<p><strong>Common Setup</strong>:</p>\n<pre><code class=\"language-nix\">let\n  inherit (nixpkgs) lib;\n  systems = [ ... ];\n  eachSystem = lib.genAttrs systems;\n</code></pre>\n<ul>\n<li>\n<p><code>systems</code>: A list of the supported systems</p>\n</li>\n<li>\n<p><code>eachSystem</code>: A Helper to map over all platforms.</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">pkgsFor = eachSystem (system:\n  import nixpkgs {\n    localSystem.system = system;\n    overlays = [(import rust-overlay) self.overlays.helix];\n  });\n</code></pre>\n<ul>\n<li>This imports <code>nixpkgs</code> for each system and applies overlays</li>\n</ul>\n<p>📦 <code>packages</code></p>\n<pre><code class=\"language-nix\">packages = eachSystem (system: {\n  inherit (pkgsFor.${system}) helix;\n  default = self.packages.${system}.helix;\n});\n</code></pre>\n<ul>\n<li>For each platform:\n<ul>\n<li>\n<p>Includes a <code>helix</code> package (defined in <code>./default.nix</code>)</p>\n</li>\n<li>\n<p>Sets <code>default</code> to <code>helix</code> (used by <code>nix build</code>, <code>nix run</code>)</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>Let’s look at the helix <code>default.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  lib,\n  rustPlatform,\n  callPackage,\n  runCommand,\n  installShellFiles,\n  git,\n  gitRev ? null,\n  grammarOverlays ? [],\n  includeGrammarIf ? _: true,\n}: let\n  fs = lib.fileset;\n\n  src = fs.difference (fs.gitTracked ./.) (fs.unions [\n    ./.envrc\n    ./rustfmt.toml\n    ./screenshot.png\n    ./book\n    ./docs\n    ./runtime\n    ./flake.lock\n    (fs.fileFilter (file: lib.strings.hasInfix \".git\" file.name) ./.)\n    (fs.fileFilter (file: file.hasExt \"svg\") ./.)\n    (fs.fileFilter (file: file.hasExt \"md\") ./.)\n    (fs.fileFilter (file: file.hasExt \"nix\") ./.)\n  ]);\n\n  # Next we actually need to build the grammars and the runtime directory\n  # that they reside in. It is built by calling the derivation in the\n  # grammars.nix file, then taking the runtime directory in the git repo\n  # and hooking symlinks up to it.\n  grammars = callPackage ./grammars.nix {inherit grammarOverlays includeGrammarIf;};\n  runtimeDir = runCommand \"helix-runtime\" {} ''\n    mkdir -p $out\n    ln -s ${./runtime}/* $out\n    rm -r $out/grammars\n    ln -s ${grammars} $out/grammars\n  '';\nin\n  rustPlatform.buildRustPackage (self: {\n    cargoLock = {\n      lockFile = ./Cargo.lock;\n      # This is not allowed in nixpkgs but is very convenient here: it allows us to\n      # avoid specifying `outputHashes` here for any git dependencies we might take\n      # on temporarily.\n      allowBuiltinFetchGit = true;\n    };\n\n    nativeBuildInputs = [\n      installShellFiles\n      git\n    ];\n\n    buildType = \"release\";\n\n    name = with builtins; (fromTOML (readFile ./helix-term/Cargo.toml)).package.name;\n    src = fs.toSource {\n      root = ./.;\n      fileset = src;\n    };\n\n    # Helix attempts to reach out to the network and get the grammars. Nix doesn't allow this.\n    HELIX_DISABLE_AUTO_GRAMMAR_BUILD = \"1\";\n\n    # So Helix knows what rev it is.\n    HELIX_NIX_BUILD_REV = gitRev;\n\n    doCheck = false;\n    strictDeps = true;\n\n    # Sets the Helix runtime dir to the grammars\n    env.HELIX_DEFAULT_RUNTIME = \"${runtimeDir}\";\n\n    # Get all the application stuff in the output directory.\n    postInstall = ''\n      mkdir -p $out/lib\n      installShellCompletion ${./contrib/completion}/hx.{bash,fish,zsh}\n      mkdir -p $out/share/{applications,icons/hicolor/{256x256,scalable}/apps}\n      cp ${./contrib/Helix.desktop} $out/share/applications/Helix.desktop\n      cp ${./logo.svg} $out/share/icons/hicolor/scalable/apps/helix.svg\n      cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps/helix.png\n    '';\n\n    meta.mainProgram = \"hx\";\n  })\n</code></pre>\n<h3>Breaking Down <code>helix/default.nix</code></h3>\n<details>\n<summary> ✔️ Click to Expand `helix/default.nix` breakdown </summary>\n<p>This <code>default.nix</code> file is a Nix derivation that defines how to build the Helix\neditor itself. It’s designed to be called by the main <code>flake.nix</code> as part of its\n<code>packages</code> output.</p>\n<p>Here’s a breakdown of its components:</p>\n<ol>\n<li><strong>Function Arguments</strong>:</li>\n</ol>\n<pre><code class=\"language-nix\">{\n  lib,\n  rustPlatform,\n  callPackage,\n  runCommand,\n  installShellFiles,\n  git,\n  gitRev ? null,\n  grammarOverlays ? [],\n  includeGrammarIf ? _: true,\n}:\n</code></pre>\n<p><code>lib</code>: The Nixpkgs <code>lib</code> (library) functions, essential for common operations\nlike <code>fileset</code> and <code>strings</code>.</p>\n<p><code>rustPlatform</code>: A helper function from Nixpkgs specifically for building Rust\nprojects. It provides a <code>buildRustPackage</code> function, which simplifies the\nprocess significantly.</p>\n<p><code>callPackage</code>: A Nixpkgs function used to instantiate a Nix expression (like\n<code>grammars.nix</code>) with its dependencies automatically supplied from the current\nNix environment.</p>\n<p><code>runCommand</code>: A Nixpkgs primitive that creates a derivation by running a shell\ncommand. It’s used here to construct the <code>runtimeDir</code>.</p>\n<p><code>installShellFiles</code>: A utility from Nixpkgs for installing shell completion\nfiles.</p>\n<p><code>git</code>: The Git package, needed for determining the <code>gitRev</code>.</p>\n<p><code>gitRev ? null</code>: The Git revision of the Helix repository. It’s an optional\nargument, defaulting to null. This is passed in from the main <code>flake.nix</code>.</p>\n<p><code>grammarOverlays ? []</code>: An optional list of overlays for grammars, allowing\ncustomization.</p>\n<p><code>includeGrammarIf ? _: true</code>: An optional function to control which grammars are\nincluded.</p>\n<ol start=\"2\">\n<li><strong>Local Variables</strong> (<code>let ... in</code>)</li>\n</ol>\n<pre><code class=\"language-nix\">let\n  fs = lib.fileset;\n\n  src = fs.difference (fs.gitTracked ./.) (fs.unions [\n    ./.envrc\n    ./rustfmt.toml\n    ./screenshot.png\n    ./book\n    ./docs\n    ./runtime\n    ./flake.lock\n    (fs.fileFilter (file: lib.strings.hasInfix \".git\" file.name) ./.)\n    (fs.fileFilter (file: file.hasExt \"svg\") ./.)\n    (fs.fileFilter (file: file.hasExt \"md\") ./.)\n    (fs.fileFilter (file: file.hasExt \"nix\") ./.)\n  ]);\n\n  grammars = callPackage ./grammars.nix { inherit grammarOverlays includeGrammarIf; };\n  runtimeDir = runCommand \"helix-runtime\" {} ''\n    mkdir -p $out\n    ln -s ${./runtime}/* $out\n    rm -r $out/grammars\n    ln -s ${grammars} $out/grammars\n  '';\nin\n</code></pre>\n<p><code>fs = lib.fileset;</code>: Aliases <code>lib.fileset</code> for convenient file set operations.</p>\n<p><code>src</code>: This is a crucial part. It defines the source files that will be used to\nbuild Helix by:</p>\n<ul>\n<li>\n<p>Taking all Git-tracked files in the current directory (<code>fs.gitTracked ./.</code>).</p>\n</li>\n<li>\n<p>Excluding configuration files (e.g., <code>.envrc</code>, <code>flake.lock</code>), documentation\n(<code>.md</code>), images (<code>.svg</code>), and Nix files (<code>.nix</code>) using <code>fs.difference</code> and\n<code>fs.unions</code>. This ensures a clean build input, reducing Nix store size and\navoiding unnecessary rebuilds.</p>\n</li>\n<li>\n<p><code>grammars</code>: Builds syntax grammars by calling <code>grammars.nix</code>, passing\n<code>grammarOverlays</code> (for customizing grammar builds) and <code>includeGrammarIf</code> (a\nfilter for selecting grammars).</p>\n</li>\n<li>\n<p><code>runtimeDir</code>: Creates a runtime directory for Helix by:</p>\n<ul>\n<li>\n<p>Symlinking the <code>runtime</code> directory from the source.</p>\n</li>\n<li>\n<p>Replacing the <code>grammars</code> subdirectory with a symlink to the <code>grammars</code>\nderivation, ensuring Helix uses Nix-managed grammars.</p>\n</li>\n</ul>\n</li>\n</ul>\n<ol start=\"3\">\n<li><strong>The Build Derivation</strong> (<code>rustPlatform.buildRustPackage</code>)</li>\n</ol>\n<p>The core of this <code>default.nix</code> is the <code>rustPlatform.buildRustPackage</code> call,\nwhich is a specialized builder for Rust projects:</p>\n<pre><code class=\"language-nix\">in\n  rustPlatform.buildRustPackage (self: {\n    cargoLock = {\n      lockFile = ./Cargo.lock;\n      # ... comments ...\n      allowBuiltinFetchGit = true;\n    };\n</code></pre>\n<p><code>cargoLock</code>: Specifies how Cargo dependencies are handled.</p>\n<p><code>lockFile = ./Cargo.lock;</code> Points to the <code>Cargo.lock</code> file for reproducible\nbuilds.</p>\n<p><code>allowBuiltinFetchGit = true</code>: Allows Cargo to fetch Git dependencies directly\nfrom repositories specified in <code>Cargo.lock</code>. This is discouraged in Nixpkgs\nbecause it can break build reproducibility, but it’s used here for convenience\nduring development, eliminating the need to manually specify <code>outputHashes</code> for\nGit dependencies.</p>\n<pre><code class=\"language-nix\">nativeBuildInputs = [\n      installShellFiles\n      git\n    ];\n</code></pre>\n<p><code>nativeBuildInputs</code>: Are tools needed during the build process but not\nnecessarily at runtime.</p>\n<pre><code class=\"language-nix\">buildType = \"release\";\n</code></pre>\n<p><code>buildType</code>: Specifies that Helix should be built in “release” mode (optimized).</p>\n<pre><code class=\"language-nix\">name = with builtins; (fromTOML (readFile ./helix-term/Cargo.toml)).package.name;\n    src = fs.toSource {\n      root = ./.;\n      fileset = src;\n    };\n</code></pre>\n<p><code>name</code>: Dynamically sets the package name by reading it from the <code>Cargo.toml</code>\nfile.</p>\n<p><code>src</code>: Uses the <code>src</code> file set defined earlier as the source for the build.</p>\n<pre><code class=\"language-nix\"># Helix attempts to reach out to the network and get the grammars. Nix doesn't allow this.\n    HELIX_DISABLE_AUTO_GRAMMAR_BUILD = \"1\";\n\n    # So Helix knows what rev it is.\n    HELIX_NIX_BUILD_REV = gitRev;\n</code></pre>\n<p><strong>Environment Variables</strong>: Sets environment variables that Helix uses.</p>\n<p><code>HELIX_DISABLE_AUTO_GRAMMAR_BUILD = \"1\"</code>: Prevents Helix from downloading\ngrammars during the build, as Nix’s sandboxed environment disallows network\naccess. Instead, grammars are provided via the <code>runtimeDir</code> derivation.</p>\n<p><code>HELIX_NIX_BUILD_REV = gitRev</code>: Embeds the specified Git revision (or <code>null</code> if\nunspecified) into the Helix binary, allowing Helix to display its version or\ncommit hash.</p>\n<pre><code class=\"language-nix\">doCheck = false;\n   strictDeps = true;\n</code></pre>\n<p><code>doCheck = false;</code>: Skips running tests during the build. This is common for\nfaster builds, especially in CI/CD, but tests are often run in a separate\n<code>checks</code> output (as seen in the <code>flake.nix</code>).</p>\n<p><code>strictDeps = true;</code>: Ensures that all dependencies are explicitly declared.</p>\n<pre><code class=\"language-nix\"># Sets the Helix runtime dir to the grammars\nenv.HELIX_DEFAULT_RUNTIME = \"${runtimeDir}\";\n</code></pre>\n<pre><code class=\"language-nix\"># Sets the Helix runtime dir to the grammars\nenv.HELIX_DEFAULT_RUNTIME = \"${runtimeDir}\";\n</code></pre>\n<p><code>env.HELIX_DEFAULT_RUNTIME</code>: Tells Helix where to find its runtime files\n(including the Nix-managed grammars).</p>\n<pre><code class=\"language-nix\"># Get all the application stuff in the output directory.\npostInstall = ''\n  mkdir -p $out/lib\n  installShellCompletion ${./contrib/completion}/hx.{bash,fish,zsh}\n  mkdir -p $out/share/{applications,icons/hicolor/{256x256,scalable}/apps}\n  cp ${./contrib/Helix.desktop} $out/share/applications/Helix.desktop\n  cp ${./logo.svg} $out/share/icons/hicolor/scalable/apps/helix.svg\n  cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps/helix.png\n'';\n</code></pre>\n<p><code>postInstall</code>: A shell script that runs after the main build is complete. This\nis used for installing additional files that are part of the Helix distribution\nbut not directly built by Cargo.</p>\n<p>Installs shell completion files (<code>hx.bash</code>, <code>hx.fish</code>, <code>hx.zsh</code>). This enables\ntab completion.</p>\n<p>Installs desktop entry files (<code>Helix.desktop</code>) and icons (<code>logo.svg</code>,\n<code>helix.png</code>) for desktop integration for GUI environments.</p>\n<pre><code class=\"language-nix\">    meta.mainProgram = \"hx\";\n\n})\n</code></pre>\n<p><code>meta.mainProgram</code>: Specifies the primary executable provided by this package,\nallowing <code>nix run</code> to automatically execute <code>hx</code>.</p>\n<p>A lot going on in this derivation!</p>\n</details>\n<h3>Making Actual Changes</h3>\n<ol>\n<li>Locate the <code>packages</code> output section. It looks like this:</li>\n</ol>\n<pre><code class=\"language-nix\">packages = eachSystem (system: {\n      inherit (pkgsFor.${system}) helix;\n      /*\n      The default Helix build. Uses the latest stable Rust toolchain, and unstable\n      nixpkgs.\n\n      The build inputs can be overridden with the following:\n\n      packages.${system}.default.override { rustPlatform = newPlatform; };\n\n      Overriding a derivation attribute can be done as well:\n\n      packages.${system}.default.overrideAttrs { buildType = \"debug\"; };\n      */\n      default = self.packages.${system}.helix;\n    });\n</code></pre>\n<ol start=\"2\">\n<li>Modify the <code>default</code> package. The comments actually tell us exactly how to do\nthis. We want to use <code>overrideAttrs</code> to change the <code>buildType</code></li>\n</ol>\n<p>Change this line:</p>\n<pre><code class=\"language-nix\">default = self.packages.${system}.helix;\n</code></pre>\n<p>To this:</p>\n<pre><code class=\"language-nix\">default = self.packages.${system}.helix.overrideAttrs { buildType = \"debug\"; };\n</code></pre>\n<ul>\n<li>This tells Nix to take the standard Helix package definition and override one\nof its internal attributes (<code>buildType</code>) to “debug” instead of “release”.</li>\n</ul>\n<ol start=\"3\">\n<li>Build the “Hacked” Helix:</li>\n</ol>\n<pre><code class=\"language-bash\">nix build\n</code></pre>\n<ul>\n<li>Nix will now rebuild Helix, but this time, it will compile it in debug mode.\nYou’ll likely notice the build takes a bit longer, and the resulting binary\nwill be larger due to the included debugging symbols.</li>\n</ul>\n<ol start=\"4\">\n<li>Run the Debug Binary:</li>\n</ol>\n<pre><code class=\"language-bash\">./result/bin/hx\n</code></pre>\n<ul>\n<li>You’re now running your custom-built debug version of Helix! This is useful if\nyou were, for example, attatching a debugger.</li>\n</ul>\n<p>This is a simple yet powerful “hack” that demonstrates how easily you can modify\nthe behavior of a package defined within a Nix flake without changing the\noriginal source code or the upstream flake directly. You’re simply telling Nix\nhow you’d like your version of the package to be built.</p>\n<h3>Another way to Modify Behavior</h3>\n<p>Since we are already familiar with the structure and behavior of Helix’s\n<code>flake.nix</code>, we can leverage that understanding to create our own Nix flake. By\nanalyzing how Helix organizes its <code>inputs</code>, <code>outputs</code>, and package definitions,\nwe gain the confidence to modify and extend a flake’s functionality to suit our\nspecific needs—whether that’s customizing builds, adding overlays, or\nintegrating with home-manager.</p>\n<ol>\n<li>Create a <code>flake.nix</code> in your own directory (outside the helix repo):</li>\n</ol>\n<pre><code class=\"language-nix\">{\n  description = \"Customized Helix build with debug features\";\n\n  inputs = {\n    helix.url = \"github:helix-editor/helix\";\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    rust-overlay = {\n      url = \"github:oxalica/rust-overlay\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n  outputs = {\n    self,\n    helix,\n    nixpkgs,\n    rust-overlay,\n  }: let\n    system = \"x86_64-linux\";\n    pkgs = import nixpkgs {\n      system = system;\n      overlays = [rust-overlay.overlay.overlays.default];\n    };\n  in {\n    packages.${system}.default = helix.packages.${system}.helix.overrideAttrs (old: {\n      buildType = \"debug\";\n\n      # Add additional cargo features\n      cargoBuildFlags =\n        (old.cargoBuildFlags or [])\n        ++ [\n          \"--features\"\n          \"tokio-console\"\n        ];\n\n      # Inject custom RUSTFLAGS\n      RUSTFLAGS = (old.RUSTFLAGS or \"\") + \" -C debuginfo=2 -C opt-level=1\";\n    });\n  };\n}\n</code></pre>\n<p>Check it:</p>\n<pre><code class=\"language-bash\">nix flake check\nwarning: creating lock file '\"/home/jr/world/flake.lock\"':\n• Added input 'helix':\n    'github:helix-editor/helix/8961ae1dc66633ea6c9f761896cb0d885ae078ed?narHash=sha256-f14perPUk%2BH15GyGRbg0Akqhn3rxFnc6Ez5onqpzu6A%3D' (2025-05-29)\n• Added input 'helix/nixpkgs':\n    'github:nixos/nixpkgs/5135c59491985879812717f4c9fea69604e7f26f?narHash=sha256-Vr3Qi346M%2B8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic%3D' (2025-02-26)\n• Added input 'helix/rust-overlay':\n    'github:oxalica/rust-overlay/d342e8b5fd88421ff982f383c853f0fc78a847ab?narHash=sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M%3D' (2025-02-27)\n• Added input 'helix/rust-overlay/nixpkgs':\n    follows 'helix/nixpkgs'\n• Added input 'nixpkgs':\n    'github:nixos/nixpkgs/96ec055edbe5ee227f28cdbc3f1ddf1df5965102?narHash=sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg%3D' (2025-05-28)\n• Added input 'rust-overlay':\n    'github:oxalica/rust-overlay/405ef13a5b80a0a4d4fc87c83554423d80e5f929?narHash=sha256-k0nhPtkVDQkVJckRw6fGIeeDBktJf1BH0i8T48o7zkk%3D' (2025-05-30)\n• Added input 'rust-overlay/nixpkgs':\n    follows 'nixpkgs'\n</code></pre>\n<ul>\n<li>The <code>nix flake check</code> command will generate a <code>flake.lock</code> file if one doesn’t\nexist, and the warnings you see indicate that new inputs are being added and\nlocked to specific versions for reproducibility. This is expected behavior for\na new or modified flake.</li>\n</ul>\n<p>Inspect the outputs:</p>\n<pre><code class=\"language-bash\">nix flake show\npath:/home/jr/world?lastModified=1748612128&amp;narHash=sha256-WEYtptarRrrm0Jb/0PJ/b5VPqLkCk5iEenjbKYU4Xm8%3D\n└───packages\n    └───x86_64-linux\n        └───default: package 'helix-term'\n</code></pre>\n<ul>\n<li>\n<p>The <code>└───packages</code> line indicates that our flake exposes a top-level\n<code>packages</code> attribute.</p>\n</li>\n<li>\n<p><code>└───x86_64-linux</code>: System architecture specificity</p>\n</li>\n<li>\n<p><code>└───default: package 'helix-term'</code> Signifies that within the <code>x86_64-linux</code>\npackages, there’s a package named <code>default</code>. This is a special name that\nallows you to omit the package name when using commands like <code>nix build</code>.</p>\n</li>\n<li>\n<p><code>package 'helix-term'</code> This is the most direct confirmation of our “hack”. It\ntells us that our <code>default</code> package is <code>helix-term</code>. This confirms that our\n<code>overrideAttrs</code> in the <code>packages.${system}.default</code> section successfully\ntargeted and modified the Helix editor package, which is internally named\n<code>helix-term</code> by the Helix flake.</p>\n</li>\n</ul>\n<p><strong>What This Does</strong>:</p>\n<ul>\n<li>\n<p><code>overrideAttrs</code> lets you change <em>only</em> parts of the derivation without\nrewriting everything.</p>\n</li>\n<li>\n<p><code>buildType = \"debug\"</code> enables debug builds.</p>\n</li>\n<li>\n<p><code>cargoBuildFlags</code> adds extra features passed to Cargo, e.g.,\n<code>--features tokio-console</code></p>\n</li>\n<li>\n<p><code>RUSTFLAGS</code> gives you even more control over compiler behavior, optimization\nlevels, etc.</p>\n</li>\n</ul>\n<p><strong>Run It</strong>:</p>\n<pre><code class=\"language-bash\">nix run\n</code></pre>\n<p>Or drop into the dev shell:</p>\n<pre><code class=\"language-bash\">nix develop\n</code></pre>\n<ul>\n<li>(assuming you also wire in a <code>devShells</code> output)</li>\n</ul>\n<p><strong>Adding the <code>devShells</code> output</strong>:</p>\n<p>Since we already have the helix flake as an input to our own <code>flake.nix</code> we can\nnow forward or extend Helix’s <code>devShells</code> like this:</p>\n<pre><code class=\"language-nix\">outputs = { self, nixpkgs, helix, rust-overlay, ... }: {\n  devShells = helix.devShells;\n};\n</code></pre>\n<p>Or if you want to pick a specific system:</p>\n<pre><code class=\"language-nix\">outputs = { self, nixpkgs, helix, rust-overlay ... }:\n  let\n    system = \"x86_64-linux\";\n  in {\n    devShells.${system} = helix.devShells.${system};\n  };\n</code></pre>\n<p><strong>Optional: Combine with your own</strong> <code>devShell</code></p>\n<p>You can also extend or merge it with your own shell like so:</p>\n<pre><code class=\"language-nix\">outputs = { self, nixpkgs, helix, rust-overlay, ... }:\n  let\n    system = \"x86_64-linux\";\n    pkgs = import nixpkgs { inherit system; };\n  in {\n    devShells.${system} = {\n      default = pkgs.mkShell {\n        name = \"my-shell\";\n        inputsFrom = [ helix.devShells.${system}.default ];\n        buildInputs = [ pkgs.git ];\n      };\n    };\n  };\n</code></pre>\n",
      "date_published": "2025-12-05T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/cachix_devour.html",
      "url": "https://saylesss88.github.io/nix/cachix_devour.html",
      "title": "Cachix devour-flake",
      "content_html": "<h1>Cachix and the devour-flake</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>Using devour-flake to Cache All Your Flake Outputs to Cachix</p>\n<p>When working with Nix flakes, it’s common to have many outputs: packages, apps,\ndev shells, NixOS or Darwin configurations, and more. Efficiently building and\ncaching all these outputs can be challenging, especially in CI or when\ncollaborating. This is where devour-flake and Cachix shine.</p>\n<p><strong>Why Use the devour-flake?</strong></p>\n<p>By default, building all outputs of a flake with <code>nix build .#a .#b ... .#z</code> can\nbe slow and inefficient, as Nix will evaluate the flake multiple times, once for\neach output. devour-flake solves this by generating a “consumer” flake that\ndepends on all outputs, allowing you to build everything in one go with a single\nevaluation</p>\n<h2>Installation</h2>\n<p>There quite a few ways to do this, choose a method of installation from the\n<a href=\"https://github.com/srid/devour-flake\">devour-flake</a> repository and then\ncontinue with step 1.</p>\n<p>Nix will only download binaries from binary caches if they are cryptographically\nsigned with any of the keys listed in <code>nix.settings.trusted-public-keys</code>.</p>\n<pre><code class=\"language-nix\"># This is the default\nnix.settings.require-sigs = true;\n</code></pre>\n<p>Example:</p>\n<pre><code class=\"language-nix\">nix.settings = {\n  builders-use-substitutes = true;\n  substituters = [\n    \"https://cache.nixos.org\"\n  ];\n  trusted-public-keys = [\n    \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\"\n  ];\n};\n</code></pre>\n<p>You can even build it without installing with the following command:</p>\n<pre><code class=\"language-bash\">nix build github:srid/devour-flake \\\n  -L --no-link --print-out-paths \\\n  --override-input flake path/to/flake | cachix push &lt;name&gt;\n</code></pre>\n<pre><code class=\"language-bash\">nix-shell -p cachix\n</code></pre>\n<p>This will push all flake outputs to cachix if you have a valid authentication\ntoken and have created a cache already.</p>\n<p>How to Use devour-flake with Cachix</p>\n<ol>\n<li>Prerequisites</li>\n</ol>\n<ul>\n<li><strong>A Cachix cache</strong>: Create one on <a href=\"https://www.cachix.org/\">Cachix</a> and\ngenerate a “Write + Read” auth token. You’ll click the cache you just created\nand select Settings, in the settings you’ll find Auth Tokens. When in the Auth\nTokens section give your token a Description, Expiration date, and finally\nclick Generate.</li>\n</ul>\n<p>(Optional) Configure your token locally, copy your auth token for the following\ncommand:</p>\n<pre><code class=\"language-bash\">cachix authtoken &lt;YOUR_TOKEN&gt;\n# Use cachix cli for the following\ncachix use your-cache-name\n</code></pre>\n<ul>\n<li><code>cachix use</code> adds your substitutors and trusted-public-keys to your\n<code>~/.config/nix/nix.conf</code> and creates one if it doesn’t exist.</li>\n</ul>\n<p><strong>Push All Flake Inputs to Cachix</strong></p>\n<p>Replace <code>&lt;mycache&gt;</code> with the name of the cache you just created.</p>\n<pre><code class=\"language-bash\">nix flake archive --json \\\n  | jq -r '.path,(.inputs|to_entries[].value.path)' \\\n  | cachix push &lt;mycache&gt;\n</code></pre>\n<p>You should see output similar to the following:</p>\n<pre><code class=\"language-bash\">Pushing 637 paths (2702 are already present) using zstd to cache sayls8 ⏳\n\n✓ /nix/store/0aqvmjvhkar3j2f7zag2wjl4073apnvk-vimplugin-crates.nvim-2025-05-30 (734.65 KiB)\n✓ /nix/store/02wm10zck7rb836kr0h3afjxl80866dp-X-Restart-Triggers-keyd (184.00 B)\n✓ /nix/store/0asdaaax0lf1wa6m6lqqdvc8kp6qn3f6-dconf-cleanup (1008.00 B)\n✓ /nix/store/09ki2jlh6sqbn01yw6n15h8d55ihxygf-helix-tree-sitter-mojo-3d7c53b8038f9ebbb57cd2e61296180aa5c1cf64 (601.37 KiB)\n✓ /nix/store/0i2c29nldqvb9pnypvp3ika4i7fhc0ck-devour-output (312.00 B)\n✓ /nix/store/0c0mwfb78xm862a7g4h9fhgzn55zppj6-helix-term (29.88 MiB)\n✓ /nix/store/0fhdpb2qck1kbngq1dlc8lyqqadj2pb1-hyprcursor-0.1.12+date=2025-06-05_45fcc10-lib (487.30 KiB)\n✓ /nix/store/0mfpi51bswgd91l8clqcz6mxy5k5zcd4-vimplugin-auto-pairs-2019-02-27 (40.60 KiB)\n✓ /nix/store/0k2zq8y78vrhhkf658j6i45vz3y89v11-helix-tree-sitter-tcl-56ad1fa6a34ba800e5495d1025a9b0fda338d5b8 (110.25 KiB)\n✓ /nix/store/0qxmahrw935136dbxkmvrg14fgnzi6bb-vimplugin-obsidian.nvim-2025-07-01 (493.02 KiB)\n✓ /nix/store/0wjppqzcbnlf9srhr6k27pz403j3mg2j-hm-session-vars.sh (1.86 KiB)\n✓ /nix/store/0z41071z33zg1zqyasccc3cfhxj389k0-helix-tree-sitter-swift-57c1c6d6ffa1c44b330182d41717e6fe37430704 (2.77 MiB)\n✓ /nix/store/0n5f1x8lpc93zm81bxrfh6yccyngvrdl-unit-plymouth-read-write.service (1.19 KiB)\n✓ /nix/store/0z8ac35n89lv2knzaj6kkp0cfxr6pmgc-hm_face.png (300.60 KiB)\n✓ /nix/store/0zp5846pry5rknnvzz81zlvj4ghnkxp5-hyprutils-0.8.1+date=2025-07-07_a822973 (421.64 KiB)\n✓ /nix/store/118ihgwjw6kp0528igns3pnvzbszljmg-unit-dbus.service (1.34 KiB)\n✓ /nix/store/0pajdq9mfgkcdwbqp38j7d4clc9h9iik-hm_.mozillafirefoxdefault.keep (112.00 B)\n✓ /nix/store/0nlvffvpx6s8mpd2rpnqb1bl5idd16yk-hm-dconf.ini (224.00 B)\n✓ /nix/store/1fiqgqvi574rdckav0ikdh8brwdhvh69-vimplugin-alpha-nvim-2025-05-26 (69.38 KiB)\n✓ /nix/store/1fqxw31p1llag0g7wg7izq22x5msz47r-vimplugin-persistence.nvim-2025-02-25 (37.74\n</code></pre>\n<blockquote>\n<p>❗ NOTE: The effectiveness of pushing the rest to cachix depend on your\nnetwork speed. I actually noticed a slow down after pushing the <code>nix/store</code>.\nPushing the <code>nix/store</code> is rarely necessary and can be very slow and\nbandwidth-intensive. Most users will only need to push relevant outputs.</p>\n</blockquote>\n<p><strong>Push the Entire /nix/store</strong></p>\n<pre><code class=\"language-bash\">nix path-info --all | cachix push &lt;mycache&gt;\n</code></pre>\n<p><strong>Pushing shell environment</strong></p>\n<pre><code class=\"language-bash\">nix develop --profile dev-profile -c true\n# then run\ncachix push &lt;mycache&gt; dev-profile\n</code></pre>\n<ul>\n<li>For the Flake way of doing things you would create something like the\nfollowing:</li>\n</ul>\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  pkgs,\n  ...\n}: let\n  cfg = config.custom.cachix;\nin {\n  options = {\n    custom.cachix.enable = lib.mkEnableOption \"Enable custom cachix configuration\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    environment.systemPackages = with pkgs; [cachix];\n\n    # to prevent garbage collection of outputs immediately after building\n    nix.extraOptions = \"gc-keep-outputs = true\";\n    nix.settings = {\n      substituters = [\n        \"https://nix-community.cachix.org\"\n        \"https://hyprland.cachix.org\"\n        \"https://ghostty.cachix.org\"\n        \"https://neovim-nightly.cachix.org\"\n        \"https://yazi.cachix.org\"\n        \"https://helix.cachix.org\"\n        \"https://nushell-nightly.cachix.org\"\n        \"https://wezterm.cachix.org\"\n        \"https://sayls88.cachix.org\"\n        # \"https://nixpkgs-wayland.cachix.org\"\n      ];\n      trusted-public-keys = [\n        \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\"\n        \"hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc=\"\n        \"ghostty.cachix.org-1:QB389yTa6gTyneehvqG58y0WnHjQOqgnA+wBnpWWxns=\"\n        \"neovim-nightly.cachix.org-1:feIoInHRevVEplgdZvQDjhp11kYASYCE2NGY9hNrwxY=\"\n        \"yazi.cachix.org-1:Dcdz63NZKfvUCbDGngQDAZq6kOroIrFoyO064uvLh8k=\"\n        \"helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs=\"\n        \"nushell-nightly.cachix.org-1:nLwXJzwwVmQ+fLKD6aH6rWDoTC73ry1ahMX9lU87nrc=\"\n        \"wezterm.cachix.org-1:kAbhjYUC9qvblTE+s7S+kl5XM1zVa4skO+E/1IDWdH0=\"\n        \"sayls88.cachix.org-1:LT8JnboX8mKhabC3Mj/ONHb5tyrjlnsdauQkD8Lu0us=\"\n        # \"nixpkgs-wayland.cachix.org-1:3lwxaILxMRkVhehr5StQprHdEo4IrE8sRho9R9HOLYA=\"\n      ];\n    };\n  };\n}\n</code></pre>\n<ul>\n<li>\n<p>The sayls8 entries are my custom cache. To find your trusted key go to the\ncachix website, click on your cache and it is listed near the top.</p>\n</li>\n<li>\n<p>I enable this with <code>custom.cachix.enable = true;</code> in my <code>configuration.nix</code> or\nequivalent.</p>\n</li>\n<li>\n<p>Another option is to use the top-level <code>nixConfig</code> attribute for adding your\nsubstitutors and trusted-public-keys. You only need to choose 1 method FYI:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">{\n  description = \"NixOS &amp; Flake Config\";\n\n# the nixConfig here only affects the flake itself, not the system configuration!\n  nixConfig = {\n    experimental-features = [ \"nix-command\" \"flakes\" ];\n    trusted-users = [ \"ryan\" ];\n\n    substituters = [\n      # replace official cache with a mirror located in China\n      \"https://mirrors.ustc.edu.cn/nix-channels/store\"\n      \"https://cache.nixos.org\"\n    ];\n\n    # nix community's cache server\n    extra-substituters = [\n      \"https://nix-community.cachix.org\"\n      \"https://nixpkgs-wayland.cachix.org\"\n    ];\n    extra-trusted-public-keys = [\n      \"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=\"\n      \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\"\n      \"nixpkgs-wayland.cachix.org-1:3lwxaILxMRkVhehr5StQprHdEo4IrE8sRho9R9HOLYA=\"\n    ];\n  };\n# ... snip\n</code></pre>\n<blockquote>\n<p>❗️ WARNING: <code>trusted-users</code> is a list of users who are allowed to bypass many\nof Nix’s normal safety restrictions and change daemon-level settings. Keep\n<code>trusted-users</code> as small as possible (often just <code>root</code> and maybe your own\nuser on a single-user box)</p>\n</blockquote>\n<ol start=\"2\">\n<li>Building and Caching All Outputs</li>\n</ol>\n<p>You can build and push all outputs of your flake to Cachix using the following\ncommand when in your flake directory:</p>\n<pre><code class=\"language-bash\">nix build github:srid/devour-flake \\\n -L --no-link --print-out-paths \\\n --override-input flake . \\\n | cachix push &lt;your-cache-name&gt;\n</code></pre>\n<ul>\n<li>\n<p>Replace <code>your-cache-name</code> with your actual Cachix cache name.</p>\n<p>This command will:</p>\n</li>\n<li>\n<p>Use devour-flake to enumerate and build all outputs of your flake (including\npackages, devShells, NixOS configs, etc.)</p>\n</li>\n<li>\n<p>Pipe the resulting store paths to cachix push, uploading them to your binary\ncache.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Example</li>\n</ol>\n<p>Suppose your cache is named my-flake-cache:</p>\n<pre><code class=\"language-bash\">nix build github:srid/devour-flake \\\n -L --no-link --print-out-paths \\\n --override-input flake . \\\n | cachix push my-flake-cache\n</code></pre>\n<ol start=\"4\">\n<li>Integration in CI</li>\n</ol>\n<p>This approach is particularly useful in CI pipelines, where you want to ensure\nall outputs are built and cached for collaborators and future builds. You can\nadd the above command to your CI workflow, ensuring the Cachix auth token is\nprovided as a secret</p>\n<ol start=\"5\">\n<li>Advanced: Using as a Nix App</li>\n</ol>\n<p>You can add devour-flake as an input to your flake for local development:</p>\n<pre><code class=\"language-nix\">{\n  inputs = {\n    devour-flake.url = \"github:srid/devour-flake\";\n    devour-flake.flake = false;\n  };\n}\n</code></pre>\n<p>And in your flake’s <code>outputs</code>, add an overlay that makes <code>devour-flake</code>\navailable in your package set:</p>\n<pre><code class=\"language-nix\">outputs = { self, nixpkgs, devour-flake, ... }@inputs: {\n  overlays.default = final: prev: {\n    devour-flake = import devour-flake { inherit (prev) pkgs; };\n  };\n\n  # Example: Add devour-flake to your devShell\n  devShells.x86_64-linux.default = let\n    pkgs = import nixpkgs {\n      system = \"x86_64-linux\";\n      overlays = [ self.overlays.default ];\n    };\n  in pkgs.mkShell {\n    buildInputs = [ pkgs.devour-flake ];\n  };\n};\n</code></pre>\n<p>Use devour-flake in your devShell:</p>\n<pre><code class=\"language-bash\">nix develop\n</code></pre>\n<p>You’ll have the <code>devour-flake</code> command available for local use, so you can\nquickly build and push all outputs as needed.</p>\n<blockquote>\n<p>TIP: Alternatively, use <code>devour-flake</code> as an app:</p>\n<pre><code class=\"language-nix\">apps.x86_64-linux.devour-flake = {\n type = \"app\";\n program = \"${self.packages.x86_64-linux.devour-flake}/bin/devour-flake\";\n};\n\n</code></pre>\n</blockquote>\n<p>What Gets Built and Cached?</p>\n<p><code>devour-flake</code> detects and builds all standard outputs of a flake, including:</p>\n<ul>\n<li>\n<p>packages</p>\n</li>\n<li>\n<p>apps</p>\n</li>\n<li>\n<p>checks</p>\n</li>\n<li>\n<p>devShells</p>\n</li>\n<li>\n<p>nixosConfigurations.*</p>\n</li>\n<li>\n<p>darwinConfigurations.*</p>\n</li>\n<li>\n<p>home-manager configurations</p>\n</li>\n</ul>\n<p>This ensures that everything your flake produces is available in your Cachix\ncache for fast, reproducible builds.</p>\n<hr />\n<h2>References:</h2>\n<p><a href=\"https://github.com/srid/devour-flake\">devour-flake documentation</a></p>\n<p><a href=\"https://discourse.nixos.org/t/how-to-set-up-cachix-in-flake-based-nixos-config/31781\">Discourse Cachix for Flakes</a></p>\n<p><a href=\"https://docs.cachix.org/installation#flakes\">Cachix docs: Flakes</a></p>\n<p><a href=\"https://www.tweag.io/blog/2020-06-25-eval-cache/#:~:text=The%20overhead%20for%20creating%20the,nixpkgs%20blender%20takes%204.9%20seconds.\">Tweag Evaluation Caching</a></p>\n<p><a href=\"https://scrive.github.io/nix-workshop/06-infrastructure/01-caching-nix.html\">Scrive Caching</a></p>\n",
      "date_published": "2025-12-05T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/whonix_kvm.html",
      "url": "https://saylesss88.github.io/nix/whonix_kvm.html",
      "title": "Whonix KVM on NixOS",
      "content_html": "<h1>Whonix KVM on NixOS</h1>\n<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/../images/swappy-20250901-101339.cleaned.png\" alt=\"Whonix Logo\" /></p>\n<blockquote>\n<p>⚠️ WARNING: There is no general software that can guarantee absolute anonymity\nor security; perfect security is a myth. Security is a continuous process, not\na one-time product. It also depends on time and resources: if an adversary has\nenough of either, eventual compromise is probable. However, by layering\ndefenses and following best practices, we can make attacks costly and\ntime-consuming, deterring all but highly targeted adversaries.</p>\n</blockquote>\n<p>It is highly recommended to harden your Host Machine as Type 2 hypervisors are\nonly as secure as their host. KVM is actually a type 1 hypervisor but relies on\nQEMU for emulation which is a Type 2 hypervisor. This actually makes it a sort\nof hybrid in between Type 1 and Type 2 but is in theory less secure than running\na Xen hypervisor (Type 1) on bare-metal.</p>\n<p>Whonix offers many benefits, including the convenience of running within your\ncurrent operating system without needing to reboot or use a separate Tails USB.\nIt provides similar strong anonymity protections by routing all traffic through\nTor in isolated virtual machines. The Whonix documentation is transparent about\nits limitations, which helps build trust and confidence in its security model.</p>\n<ul>\n<li>\n<p><a href=\"https://www.whonix.org/wiki/Comparison_with_Others\">Whonix Compared to Tails</a></p>\n</li>\n<li>\n<p>Tails is great but they add an add blocker to Tor that makes every Tails user\nunique from the rest of Tor Browser users reducing anonymity.</p>\n</li>\n</ul>\n<blockquote>\n<p>⚠️ Never rely solely on the Virtual Machine to protect you, if your host OS\nisn’t secure a Virtual Machine won’t protect you. If you have high threat\nmodel, you may want to choose a Host with better support for AppArmor and\nSelinux as they are highly limited on NixOS.</p>\n</blockquote>\n<p>That being said, there is a lot you can do to harden NixOS…</p>\n<h3>Harden NixOS and set up GnuPG</h3>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html\">Hardening NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/hardening_networking.html\">Hardening Networking</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/gpg-agent.html\">GnuPG and gpg-agent on NixOS</a></p>\n</li>\n</ul>\n<h3>A Few Things to Consider when using Whonix</h3>\n<ul>\n<li>\n<p>No activity conducted inside <code>Whonix-Workstation</code> can cause IP/DNS leaks so\nlong as <code>Whonix-Gateway</code> is left unchanged or only documented changes are made\nlike configuring bridges, establishing onion services and running updates.</p>\n</li>\n<li>\n<p>Whonix does not and does not claim to protect users against advanced\nadversaries such as nation state actors, if they target you, you will be\ninfected! If used correctly, Whonix can provide partial protection against\npassive surveillance programs, it all depends on whether Tor can provide\nadequate protection or not, which is not clear at this time.</p>\n</li>\n<li>\n<p>You shouldn’t use a VPN with Whonix and it is obvious that you’re using Tor\nbecause connections are made to known Tor Relays, which are publicly listed\nand identifiable.</p>\n</li>\n</ul>\n<blockquote>\n<p>⚠️It is impossible to Hide Tor use from the internet service provider (ISP).\nIt has been concluded this goal is difficult beyond practicality.\n–<a href=\"https://www.whonix.org/wiki/Hide_Tor_from_your_Internet_Service_Provider\">Whonix Hide Tor from your ISP</a></p>\n</blockquote>\n<ul>\n<li>\n<p>Millions of people use Tor daily for wholly legitimate reasons, particularly\nto assert their privacy rights when faced with countless corporate /\ngovernment network observers and censors.</p>\n</li>\n<li>\n<p>True anonymity is very difficult to successfully pull off and not something\nthat you can maintain for a long time.</p>\n<ul>\n<li><a href=\"https://www.whonix.org/wiki/Tips_on_Remaining_Anonymous\">Whonix Tips for remaining Anonymous</a></li>\n</ul>\n</li>\n</ul>\n<h2>🔑 Key Terms</h2>\n<p>Whonix is an operating system based on Debian base (Kicksecure Hardened) and the\nTor network, which is designed for maximum anonymity and security. Whonix\nconsists of two Debian based VMs, the <code>Whonix-Gateway</code> and <code>Whonix-Workstation</code>.</p>\n<p>In this case NixOS is the <strong>Host Operating System</strong>, NixOS runs the KVM kernel\nmodule, libvirtd service, and QEMU virtualization service which together enable\nhosting VMs. It is recommended to harden the host before moving on.</p>\n<p><strong>Guests</strong> are the virtualized operating systems running inside the host’s\nvirtual machines. In this case the Whonix VMs are the <strong>Guest Machines</strong>.</p>\n<p><code>Whonix-Gateway</code> the first of 2 VMs runs Tor processes and forces all traffic\nthrough the Tor network using iptables.</p>\n<p><code>Whonix-Workstation</code> the second VM, is responsible for running user applications\nsuch as the Tor Browser. The Whonix-Workstation is isolated from both the\nWhonix-Gateway and the Host OS, if an app misbehaves, it is contained within the\nisolated Whonix-Workstation. It is largely unaware of sensitive info and won’t\nleak unless an advanced adversary is able to break out of the VM.</p>\n<p>The primary goal of Whonix is to be safer than Tor alone and that no one can\nfind out the user’s IP, location, or de-anonymize the user. It offers full\nspectrum anti-tracking protection that is much safer than VPNs. Whonix provides\nthis through security by isolation, no app is trusted.</p>\n<p><code>Whonix Concept</code>: Whonix is an Isolating Proxy with an additional Transparent\nProxy, which can be optionally disabled. –Whonix Docs</p>\n<p>Since Whonix is based on Kicksecure which is based on Debian stable, you can\ntypically look up solutions in a Kicksecure, Debian, or Ubuntu forum.</p>\n<ul>\n<li>The Whonix Team recommends KVM over VirtualBox for a number of\nreasons:<a href=\"https://www.whonix.org/wiki/KVM#Why_Use_KVM_Over_VirtualBox?\">Why choose KVM over VirtualBox</a></li>\n</ul>\n<p>If you really want to use VirtualBox, I got it working off of this config:</p>\n<p>VirtualBox = Type 2 hypervisor</p>\n<details>\n<summary> ✔️ Click to Expand VirtualBox Example </summary>\n<p>Change <code>your-user</code> to your username</p>\n<pre><code class=\"language-nix\"># vbox.nix\n{\n  config,\n  lib,\n  ...\n}: let\n  cfg = config.custom.virtualbox;\nin {\n  options.custom.virtualbox = {\n    enable = lib.mkEnableOption \"Enable VirtualBox\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    virtualisation.virtualbox.host = {\n      enable = false;\n      # enableExtensionPack = true;\n    };\n\n    user.user.your-user.extraGroups = [\"vboxusers\"];\n\n    boot.kernelModules =\n      if config.hardware.cpu.amd.updateMicrocode\n      then [\"kvm-amd\"]\n      else [\"kvm-intel\"];\n  };\n}\n</code></pre>\n<p>Enable it with <code>custom.virtualbox.enable = true;</code>.</p>\n<ul>\n<li><a href=\"https://www.whonix.org/wiki/VirtualBox\">Whonix VBox Download</a></li>\n</ul>\n<p>After rebuilding with virtualbox enabled and downloading the virtualbox whonix,\nopen VirtualBox and import the Whonix file.</p>\n<p>Fix the error:: VirtualBox can’t enable the AMD-V extension. Please disable the\nKVM kernel extension:</p>\n<p>If both of these are active, they compete with each other:</p>\n<pre><code class=\"language-bash\">sudo lsmod | grep -E 'kvm|vbox'\n</code></pre>\n<p>Check the currently in use modules:</p>\n<pre><code class=\"language-bash\">modprobe -r kvm\n</code></pre>\n<p>Disable kvm and kvm_amd:</p>\n<pre><code class=\"language-bash\">sudo rmmod kvm_amd\nsudo rmmod kvm\n# To re-enable them when necessary\n# sudo modprobe kvm\n# sudo modprobe kvm_amd\n</code></pre>\n<ul>\n<li><a href=\"https://atetux.com/quick-fix-virtualbox-cant-enable-the-amd-v-extension\">Quick fix</a></li>\n</ul>\n<p>There is an opposite viewpoint,\n<a href=\"https://www.whonix.org/wiki/Dev/VirtualBox#Why_use_VirtualBox_over_KVM?\">Why choose VirtualBox over KVM</a></p>\n</details>\n<h2>Whonix-Gateway</h2>\n<p>The whonix-gateway is software designed to run Tor.</p>\n<p>The Gateway acts as a firewall and is what is routing all your traffic through\nTor.</p>\n<p>You will spend minimal time in the Gateway, it’s mainly used for Tor\nconfiguration which is reserved for advanced users.</p>\n<h3>Whonix-Workstation</h3>\n<p>All user applications should only be launched from Whonix-Workstation to ensure\nthey utilize the Tor network. (Never launch the Tor browser or any other user\napp from Whonix-Gateway.)</p>\n<p>Leaky applications can’t breakout of the Workstation, all network connections\nare forced to go through the Whonix-Gateway where they are torrified and routed\nto the internet.</p>\n<h2>Whonix KVM (Kernel Virtual Machine) on NixOS</h2>\n<p><strong>KVM</strong> (Kernel-based Virtual Machine) is a Linux kernel module that provides\nhardware-assisted virtualization.</p>\n<p>It allows the Linux kernel to act as a hypervisor, enabling virtual machines\n(VMs) to run with near-native speeds by using CPU virtualization extensions\n(Intel VT-x or AMD-V).</p>\n<p>KVM itself doesn’t handle the entire VM lifecycle; it provides the core\nvirtualization infrastructure.</p>\n<p><strong>QEMU</strong> (Quick Emulator) is an open-source user-space program that emulates\nhardware for virtual machines.</p>\n<p>When combined with KVM, QEMU uses hardware acceleration to run VMs much faster\nby offloading CPU virtualization to KVM.</p>\n<p>So, QEMU provides the device emulation and VM management interface, while KVM\nprovides the fast virtualization engine within the kernel.</p>\n<p><strong>Install Qemu-KVM</strong>:</p>\n<pre><code class=\"language-nix\">{\n  config,\n  pkgs,\n  ...\n}: {\n  ##  QEMU-KVM\n  environment.systemPackages = with pkgs; [\n    qemu\n    # Optional\n    virt-viewer\n  ];\n\n  # Virt-Manager GUI\n  programs.virt-manager.enable = true;\n  virtualisation = {\n    # libvirtd daemon\n    libvirtd = {\n      enable = true;\n      qemu = {\n        # enables a TPM emulator\n        swtpm.enable = true;\n      };\n    };\n    # allow USB device to be forwarded\n    spiceUSBRedirection.enable = true;\n  };\n  # Spice protocol improves VM display and input responsiveness\n  services.spice-vdagentd.enable = true;\n}\n</code></pre>\n<hr />\n<p>The <strong>libvirtd</strong> is the primary daemon (service) in the libvirt virtualization\nmanagement system. It runs on your host machine and acts as the core management\ncomponent for virtual machines (VMs).</p>\n<p>Add <code>libvirtd</code> &amp; <code>kvm</code> to your users <code>extraGroups</code>:</p>\n<pre><code class=\"language-nix\">users.users = {\n    your-user = {\n        extraGroups = [\n            \"libvirtd\"\n            \"kvm\"\n        ];\n    };\n};\n</code></pre>\n<p>Restart <code>libvirtd</code>:</p>\n<pre><code class=\"language-bash\">sudo systemctl restart libvirtd\n</code></pre>\n<hr />\n<h2>Network Start</h2>\n<p>Ensure KVM’s / QEMU’s default network is enabled and has started:</p>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-autostart default\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-start default\n</code></pre>\n<hr />\n<h3>Download Whonix (KVM) (stable)</h3>\n<ol>\n<li>\n<p><a href=\"https://www.whonix.org/download/libvirt/17.4.4.6/Whonix-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz\">Whonix (KVM) (stable) Download</a></p>\n</li>\n<li>\n<p>Go to <a href=\"https://www.whonix.org/wiki/KVM\">whoniix.org</a> to verify the signature.\nDownload the <code>OpenPGP Signature</code>, and the <code>Download Whonix OpenPGP Key</code>. Your\nDownloads directory will look like this:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">~/Downloads󰏫 ls\n╭───┬───────────────────────────────────────────────────────┬──────┬─────────┬───────────────╮\n│ # │                         name                          │ type │  size   │   modified    │\n├───┼───────────────────────────────────────────────────────┼──────┼─────────┼───────────────┤\n│ 0 │ Whonix-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz     │ file │  3.3 GB │ 2 minutes ago │\n│ 1 │ Whonix-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz.asc │ file │  1.0 kB │ a minute ago  │\n│ 2 │ derivative.asc                                        │ file │ 77.3 kB │ 3 minutes ago │\n╰───┴───────────────────────────────────────────────────────┴──────┴─────────┴───────────────╯\n</code></pre>\n<p>Import <code>derivative.asc</code>:</p>\n<pre><code class=\"language-bash\">gpg --import derivative.asc\n</code></pre>\n<p>Verify the Public Key:</p>\n<pre><code class=\"language-bash\">gpg --verify Whonix-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz.asc Whonix-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz\ngpg: Signature made Sun 10 Aug 2025 09:04:13 AM EDT\ngpg:                using RSA key 6E979B28A6F37C43BE30AFA1CB8D50BB77BB3C48\ngpg: Good signature from \"Patrick Schleizer &lt;adrelanos@kicksecure.com&gt;\" [unknown]\ngpg:                 aka \"Patrick Schleizer &lt;adrelanos@riseup.net&gt;\" [unknown]\ngpg:                 aka \"Patrick Schleizer &lt;adrelanos@whonix.org&gt;\" [unknown]\ngpg: WARNING: This key is not certified with a trusted signature!\ngpg:          There is no indication that the signature belongs to the owner.\nPrimary key fingerprint: 916B 8D99 C38E AF5E 8ADC  7A2A 8D66 066A 2EEA CCDA\n     Subkey fingerprint: 6E97 9B28 A6F3 7C43 BE30  AFA1 CB8D 50BB 77BB 3C48\n~/Downloads󰏫                                                                                                 09/04/2025 11:53:10 AM\n</code></pre>\n<p>Now <code>gpg --list-keys</code> will show Patrick Schleizer’s Key.</p>\n<p>It is good practice to sign your verified key and then push it to the public\nkeyserver to contribute to the web of trust but optional.</p>\n<ol start=\"3\">\n<li><a href=\"https://www.whonix.org/wiki/KVM#Decompress\">Decompress the Image</a> and follow\nthe rest of the Whonix KVM install instructions from there.</li>\n</ol>\n<p>Nixpkgs doesn’t have the <code>xz-utils</code> package but it does have the <code>xz</code> package.</p>\n<p>Nixpkgs also has <code>nixpkgs.safe-rm</code> if you wanted to follow the suggestions from\nWhonix.</p>\n<pre><code class=\"language-bash\">nix-shell -p xz safe-rm\n</code></pre>\n<pre><code class=\"language-bash\">tar -xvf Whonix*.libvirt.xz\n</code></pre>\n<hr />\n<h3>Import the Whonix VM Templates</h3>\n<p>The following commands come directly from the\n<a href=\"https://www.whonix.org/wiki/KVM#Importing_Whonix_VM_Templates\">Whonix KVM Docs Importing Whonix VM Templates</a></p>\n<ol>\n<li>Add the virtual networks. This step only needs to be done once and not with\nevery upgrade.</li>\n</ol>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-define Whonix_external*.xml\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-define Whonix_internal*.xml\n</code></pre>\n<ol start=\"2\">\n<li>Activate the virtual networks:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-autostart Whonix-External\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-start Whonix-External\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-autostart Whonix-Internal\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system net-start Whonix-Internal\n</code></pre>\n<ol start=\"3\">\n<li>Import the Whonix Gateway and Workstation images:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system define Whonix-Gateway*.xml\n</code></pre>\n<pre><code class=\"language-bash\">sudo virsh -c qemu:///system define Whonix-Workstation*.xml\n</code></pre>\n<p>After the above steps, either copy or move the <code>qcow2</code> images to\n<code>/var/lib/libvirt/images</code>:</p>\n<blockquote>\n<p>❗ It’s recommended to move the files into place, if you want to copy them you\nneed to use a special command FYI.</p>\n</blockquote>\n<pre><code class=\"language-bash\">sudo mkdir -p /var/lib/libvirt/images\n</code></pre>\n<pre><code class=\"language-bash\">sudo mv Whonix-Gateway*.qcow2 /var/lib/libvirt/images/Whonix-Gateway.qcow2\n</code></pre>\n<pre><code class=\"language-bash\">sudo mv Whonix-Workstation*.qcow2 /var/lib/libvirt/images/Whonix-Workstation.qcow2\n</code></pre>\n<h3>Cleanup</h3>\n<pre><code class=\"language-bash\">safe-rm Whonix*\n</code></pre>\n<pre><code class=\"language-bash\">safe-rm -r WHONIX*\n</code></pre>\n<h3>Launch virt-manager and start the VMs</h3>\n<pre><code class=\"language-bash\">virt-manager\n</code></pre>\n<p>From here it will take a bit to load both VMs, you can click on one and go to\n<code>Edit</code>, <code>Virtual Machine Details</code> and from there you have some options to give\nthe VM more CPUs and memory.</p>\n<p>Considering that the Whonix-Workstation is where all of the user applications\nwill be opened, it makes sense to give it more CPUs and memory.</p>\n<p>I’ve seen recommendations for a minimum of 4G of RAM for the Workstation and 2GB\nfor the Gateway.</p>\n<ul>\n<li>\n<p>Increase vCPU count for better performance</p>\n</li>\n<li>\n<p>Enable XML editing in settings</p>\n</li>\n<li>\n<p>Enable copy pasting by adding <code>&lt;clipboard copypaste=\"yes\"/&gt;</code></p>\n</li>\n</ul>\n<h2>Start Whonix-Gateway</h2>\n<p><img src=\"https://saylesss88.github.io/../images/swappy-20250901-101351.cleaned.png\" alt=\"Whonix Old Logo\" /></p>\n<p>Always start the Whonix-Gateway first.</p>\n<p>Click on Whonix-Gateway, press Play, and choose the default Persistent VM.</p>\n<p>To view the gateway press <code>Open</code>.</p>\n<p>You can use the “System Maintenance Panel” to <code>Check for Updates</code> and then\n<code>Install Updates</code>. This can also be used for user and password creation, the\ndefault user is <code>user</code> with a passwordless login.</p>\n<p>Change the password manually:</p>\n<pre><code class=\"language-bash\">sudo passwd\nchangeme\n</code></pre>\n<p>Change the passwords and disable auto-login.</p>\n<p>Run a systemcheck if it wasn’t run automatically. Click the Xfce Logo and go to\n<code>System</code>, <code>System Check</code>.</p>\n<ul>\n<li><a href=\"https://www.whonix.org/wiki/Common_CLI_Commands\">Whonix Common CLI Commands</a></li>\n</ul>\n<h2>Whonix-Workstation</h2>\n<p>Whonix-Workstation is another VM, designed to provide users with a secure and\nanonymous environment for running applications and performing online tasks.</p>\n<p>When you first launch <code>Whonix-Workstation</code>, choose the second option down or\nreboot, and then choose “Persistent Mode Sysmaint Session”. From there, you can\ngo through the same steps as you did for the Gateway.</p>\n<p>With the workstation, a security feature disables <code>sudo</code> for the default user.\nInstead of the <code>user</code> account, a separate <code>sysmaint</code> (system maintenance)\naccount is used for administrative tasks that require root privileges, such as\nupdates and package installations.</p>\n<ul>\n<li>Change all user passwords and disable auto-login</li>\n</ul>\n<p>After you get your system updated and upgraded, you’ll want to reboot the\nWorkstation and start it in the first Persistent mode available rather than the\n<code>sysmaint</code> mode.</p>\n<p>Once Workstation is running and both VMs are updated and upgraded, check that\nyour IP address is a Tor IP:</p>\n<pre><code class=\"language-bash\">curl ip.me\n#\ncurl ip.me\n</code></pre>\n<p>Each consecutive time that you run <code>curl ip.me</code>, Tor establishes a new circuit\nand you will get a different IP returned each time for as many Tor nodes are\navailable. Not that you would want to but it’s cool functionality giving us a\nvisual of the new circuit.</p>\n<p>Start Tor and check what you are fingerprinted as by typing <code>deviceinfo.me</code> into\nthe URL.</p>\n<h4>Launching Tor Browser</h4>\n<p>Click the Xfce logo and choose Tor Browser. On the first launch, you will need\nto update Tor by clicking in the top right corner.</p>\n<p>Or you can open the terminal and type:</p>\n<pre><code class=\"language-bash\">update-torbrowser\n</code></pre>\n<ul>\n<li>Every time you run the above command, the old browser will be killed, along\nwith your old browser profile, including bookmarks and passwords. If the\nupdate suggests a downgrade from your current version don’t do it, it is\nlikely a downgrade attack.</li>\n</ul>\n<p>Make sure you don’t forget to go to the Settings, Privacy and Security, and set\nthe <code>Security Level</code> to <code>Safest</code> to disable JavaScript and more before exploring\nthe dark web.</p>\n<p>Visit <code>https://check.torproject.org</code>, you should see “Congratulations. This\nbrowser is configured to use Tor.”</p>\n<p>If you need a place to start, check out <code>https://tor.taxi</code> by plugging that into\nthe URL. Always include the <code>https</code> yourself!</p>\n<blockquote>\n<p>❗ NOTE: Use HTTPS and TLS wherever possible, since Tor only encrypts traffic\nas it travels through the network of three nodes. Traffic at Exit nodes is\nvulnerable if unencrypted, because when it reaches the Exit node it is plain\ntext. Prefer the use of <code>.onion</code> services because they form a tunnel that is\nencrypted end-to-end, using a random rendezvous point within the Tor network;\nHTTPS isn’t required within Onion services. Prefer the use of <code>.onion</code>\nservices because they form a tunnel that is encrypted end-to-end, using a\nrandom rendezvous point within the Tor network; HTTPS isn’t required within\nOnion services.\n–<a href=\"https://www.whonix.org/wiki/Tor_Myths_and_Misconceptions#All_my_traffic_is_encrypted_by_default\">All my traffic is encrypted by default?</a></p>\n</blockquote>\n<h2>Live Mode</h2>\n<p>To get Whonix to perform more similarly to Tails you could run Whonix in Live\nMode. Live Mode is a privacy-focused mode where nothing is saved at shutdown,\nmaking it great for handling sensitive data.</p>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/Live_Mode\">Live Mode</a></li>\n</ul>\n<p>Same process, reboot the Workstation and Choose\n<code>LIVE Mode | USER Session | disposable use</code></p>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/Anti-Forensics_Precautions\">Anti Forensics Precautions</a></li>\n</ul>\n<h2>Download and Verify Kicksecure KVM</h2>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/KVM\">Kicksecure KVM wiki</a></li>\n</ul>\n<ol>\n<li>\n<p><a href=\"https://www.kicksecure.com/download/libvirt/17.4.4.6/Kicksecure-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz\">Download Kicksecure Xfce (KVM) (stable) (FREE!)</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/download/libvirt/17.4.4.6/Kicksecure-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz.asc\">Download OpenPGP Signature</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/keys/derivative.asc\">Download Kicksecure OpenPGP Key</a></p>\n</li>\n<li>\n<p>Import the <code>derivative.asc</code> file:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">gpg --import derivative.asc\n</code></pre>\n<ol start=\"5\">\n<li>Make sure both files are done downloading and run the following to verify,\nyour file names might be slightly different:</li>\n</ol>\n<pre><code class=\"language-bash\">gpg --verify Kicksecure-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz.asc Kicksecure-Xfce-17.4.4.6.Intel_AMD64.qcow2.libvirt.xz\ngpg: Signature made Sun 10 Aug 2025 07:32:52 AM EDT\ngpg:                using RSA key 6E979B28A6F37C43BE30AFA1CB8D50BB77BB3C48\ngpg: Good signature from \"Patrick Schleizer &lt;adrelanos@kicksecure.com&gt;\" [unknown]\ngpg:                 aka \"Patrick Schleizer &lt;adrelanos@riseup.net&gt;\" [unknown]\ngpg:                 aka \"Patrick Schleizer &lt;adrelanos@whonix.org&gt;\" [unknown]\ngpg: WARNING: This key is not certified with a trusted signature!\ngpg:          There is no indication that the signature belongs to the owner.\nPrimary key fingerprint: 916B 8D99 C38E AF5E 8ADC  7A2A 8D66 066A 2EEA CCDA\n     Subkey fingerprint: 6E97 9B28 A6F3 7C43 BE30  AFA1 CB8D 50BB 77BB 3C48\n</code></pre>\n<ol start=\"6\">\n<li><strong>Decompress</strong></li>\n</ol>\n<pre><code class=\"language-bash\">tar -xvf Kicksecure*.libvirt.xz\n</code></pre>\n<p>Don’t use <code>unxz</code>!</p>\n<h3>Resources</h3>\n<ul>\n<li>\n<p><a href=\"https://www.whonix.org/wiki/Documentation\">Whonix Docs</a></p>\n</li>\n<li>\n<p><a href=\"https://www.whonix.org/wiki/About\">Whonix Overview</a></p>\n</li>\n<li>\n<p><a href=\"https://www.whonix.org/wiki/Dev/Technical_Introduction\">Whonix Technical Intro</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Computer_Security_Introduction\">Kicksecure Computer Security Intro</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Computer_Security_Introduction#Advanced_Security_Guide\">Kicksecure Advanced Security Guide</a></p>\n</li>\n</ul>\n<p>k\n<a href=\"https://www.kicksecure.com/wiki/System_Hardening_Checklist\">System Hardening Checklist</a></p>\n",
      "date_published": "2025-12-04T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/functions/functions_and_modules_2.2.html",
      "url": "https://saylesss88.github.io/functions/functions_and_modules_2.2.html",
      "title": "Functions and NixOS Modules",
      "content_html": "<h1>Functions and NixOS Modules</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>When you start exploring NixOS configurations or tools like Home Manager, you’ll\nencounter a concept called Nix Modules. Modules are also functions, but they\nbehave differently regarding their arguments, which can be a source of\nconfusion.</p>\n<p><strong>What are NixOS Modules</strong>?</p>\n<p>Nix Modules are a powerful system built on top of basic Nix functions, primarily\nused for declarative system configurations (like NixOS, Home Manager, NixOps,\netc.). They allow you to define parts of your system configuration in separate\nfiles that are then composed together.</p>\n<p>Each module is typically a Nix function that returns an attribute set with\nspecific keys like <code>options</code>, <code>config</code>, and <code>imports</code>.</p>\n<p><strong>Automatic Arguments in Modules</strong></p>\n<p>Unlike the functions we’ve been writing, Nix’s module system automatically\npasses a standard set of arguments to every module function it evaluates. You\ndon’t explicitly pass these arguments when you <code>import</code> a module file; the\nmodule system handles it for you.</p>\n<p>The most common automatic arguments you’ll see are:</p>\n<ul>\n<li>\n<p><code>config</code>: The aggregated configuration options of all modules combined. This\nis what you use to read other configuration values.</p>\n</li>\n<li>\n<p><code>options</code>: The definitions of all available configuration options across all\nmodules.</p>\n</li>\n<li>\n<p><code>pkgs</code>: The standard Nixpkgs set, equivalent to <code>import &lt;nixpkgs&gt; {}</code>. This is\nincredibly convenient as you don’t need to import it in every module.</p>\n</li>\n<li>\n<p><code>lib</code>: The Nixpkgs utility library (<code>pkgs.lib</code>), providing helper functions\nfor common tasks.</p>\n</li>\n<li>\n<p><code>specialArgs</code>: An attribute set of extra arguments to be passed to the module\nfunctions.</p>\n</li>\n</ul>\n<p>A typical module might start like this:</p>\n<pre><code class=\"language-nix\"># Example NixOS module\n{ config, pkgs, lib, ... }: # These arguments are passed automatically by the module system\n{\n  # ... module options and configuration\n  environment.systemPackages = [ pkgs.firefox pkgs.git ];\n  services.nginx.enable = true;\n  # ...\n}\n</code></pre>\n<p>In the above module, the only required argument is <code>pkgs</code> because we explicitly\nuse it in the module (i.e. <code>pkgs.firefox</code>). Editors have pretty good support for\nletting you know if you’re missing arguments or have unnecessary ones. <code>config</code>,\nand <code>lib</code> and would be required if we were setting any options in this module.</p>\n<p>This automatic passing of arguments is a core feature of the module system that\nsimplifies writing configurations, as you always have access to <code>pkgs</code>, <code>lib</code>,\nand the evolving <code>config</code> and <code>options</code> without boilerplate.</p>\n<h4><code>specialArgs</code>: Passing Custom Arguments to Modules</h4>\n<p>While the module system passes a standard set of arguments automatically, what\nif you need to pass additional, custom data to your modules that isn’t part of\nthe standard <code>config</code>, <code>pkgs</code>, <code>lib</code>, or <code>options</code>? This is where <code>specialArgs</code>\ncomes in.</p>\n<p><code>specialArgs</code> is an attribute you can pass to the <code>import</code> function when you\nload a module (or a set of modules). It’s typically used to provide data that\nyour modules need but isn’t something Nixpkgs would normally manage.</p>\n<p>For example, in a <code>configuration.nix</code>:</p>\n<pre><code class=\"language-nix\"># From your configuration.nix\n{ config, pkgs, lib, ... }: # Standard module arguments\n\nlet\n  myCustomValue = \"helloWorld\";\nin\n{\n  # ... imports all modules, including your custom ones\n  imports = [\n    ./hardware-configuration.nix\n    ./my-webserver-module.nix\n  ];\n\n  # This is where specialArgs would be used (often in import statements)\n  # Example: passing a custom value to ALL modules:\n  # (in module context, this is more complex, but conceptually)\n  # let\n  #   allModules = [ ./my-module.nix ];\n  # in\n  # lib.nixosSystem {\n  #   modules = allModules;\n  #   specialArgs = {\n  #     username = \"johndoe\";\n  #     mySecretKey = \"/run/keys/ssh_key\";\n  #   };\n  #   # ...\n  # };\n}\n</code></pre>\n<p>And then, inside <code>my-webserver-module.nix</code>:</p>\n<pre><code class=\"language-nix\"># my-webserver-module.nix\n{ config, pkgs, lib, username, mySecretKey, ... }: # username and mySecretKey come from specialArgs\n{\n  # ... use username and mySecretKey in your module\n  users.users.${username} = {\n    isNormalUser = true;\n    extraGroups = [ \"wheel\" \"networkmanager\" ];\n    # ...\n  };\n  # ...\n}\n</code></pre>\n<p>Any argument listed in a module’s function signature that is not one of the\nstandard <code>config</code>, <code>pkgs</code>, <code>lib</code>, <code>options</code> (or <code>pkgs.callPackage</code>, etc., which\nare often implicit through <code>pkgs</code>) must be provided via <code>specialArgs</code> at the\npoint where the modules are composed.</p>\n<p>Any values listed in a module that aren’t automatically passed via Nixpkgs must\nbe explicitly provided through <code>specialArgs</code>.</p>\n<h3><code>specialArgs</code> and <code>extraSpecialArgs</code> with Flakes</h3>\n<p>NixOS modules use <code>specialArgs</code> and Home-Manager uses <code>extraSpecialArgs</code> to\nallow you to pass extra arguments.</p>\n<p>Or with Flakes it would look like this:</p>\n<pre><code class=\"language-nix\">{\n  description = \"My Flake\";\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    home-manager.url = \"github:nix-community/home-manager\";\n    home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n   };\n\n  outputs = { self, nixpkgs, home-manager, ... }:\n    let\n      lib = nixpkgs.lib;\n      pkgs = nixpkgs.legacyPackages.${\"x86_64-linux\"};\n      system = \"x86_64-linux\";\n  host = \"magic\";\n  username = \"jr\";\n  userVars = {\n    timezone = \"America/New_York\";\n    locale = \"en_US.UTF-8\";\n    gitUsername = \"TSawyer87\";\n    dotfilesDir = \"~/.dotfiles\";\n    wm = \"hyprland\";\n    browser = \"firefox\";\n    term = \"ghostty\";\n    editor = \"hx\";\n    keyboardLayout = \"us\";\n  };\n    in {\n      nixosConfigurations = {\n        YOURHOSTNAME = lib.nixosSystem {\n          system = \"x86_64-linux\";\n          modules = [ ./configuration.nix ];\n          specialArgs = {\n            inherit userVars; # == userVars = userVars;\n            inherit host;\n            inherit username;\n          };\n        };\n      };\n      homeConfigurations = {\n        USERNAME = home-manager.lib.homeManagerConfiguration {\n          inherit pkgs;\n          modules = [ ./home.nix ];\n          extraSpecialArgs = {\n            inherit userVars;\n            inherit host;\n            inherit username;\n            # or it can be written like this:\n            # inherit userVars host username;\n          };\n        };\n      };\n    };\n}\n</code></pre>\n<p>Now if I want to use any of these arguments in modules I can by any module file\nreferenced by my configuration.</p>\n<p>For example, the following is a <code>git.nix</code> module that uses the variables from\nthe flake passed from <code>extraSpecialArgs</code> in this case because it’s a\nhome-manager module:</p>\n<pre><code class=\"language-nix\"># git.nix\n{ userVars, ... }: {\n  programs = {\n    git = {\n      enable = true;\n      userName = userVars.gitUsername;\n    };\n  };\n}\n</code></pre>\n<table><thead><tr><th style=\"text-align: left\">Feature</th><th style=\"text-align: left\">Regular Nix Function (e.g., <code>hello.nix</code>)</th><th style=\"text-align: left\">Nix Module (e.g., <code>my-config-module.nix</code>)</th></tr></thead><tbody>\n<tr><td style=\"text-align: left\"><strong>Arguments</strong></td><td style=\"text-align: left\"><strong>You must explicitly pass every single argument.</strong></td><td style=\"text-align: left\"><strong>Automatically receives <code>config</code>, <code>pkgs</code>, <code>lib</code>, <code>options</code>, etc.</strong></td></tr>\n<tr><td style=\"text-align: left\"><strong>Custom Args</strong></td><td style=\"text-align: left\">Passed directly in the function call.</td><td style=\"text-align: left\">Passed via <code>specialArgs</code> when the modules are composed.</td></tr>\n<tr><td style=\"text-align: left\"><strong>Boilerplate</strong></td><td style=\"text-align: left\">Often needs <code>pkgs = import &lt;nixpkgs&gt; {};</code> if not explicitly passed.</td><td style=\"text-align: left\"><code>pkgs</code> and <code>lib</code> are always available automatically.</td></tr>\n<tr><td style=\"text-align: left\"><strong>Purpose</strong></td><td style=\"text-align: left\">Defines a package, a utility, or a single value.</td><td style=\"text-align: left\">Defines a reusable part of a declarative system configuration.</td></tr>\n</tbody></table>\n",
      "date_published": "2025-11-30T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/vcs/git.html",
      "url": "https://saylesss88.github.io/vcs/git.html",
      "title": "Git",
      "content_html": "<h1>Version Control with Git</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![Git Logo](../images/git1.png) -->\n<p>⚠️ <strong>Important</strong>: Never commit secrets (passwords, API keys, tokens, etc.) in\nplain text to your Git repository. If you plan to publish your NixOS\nconfiguration, always use a secrets management tool like sops-nix or agenix to\nkeep sensitive data safe. See the\n<a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">Sops-Nix Guide</a>\nfor details.</p>\n<p>It’s also important to understand that <strong>all files in the <code>/nix/store</code> are\nworld-readable by default</strong> This has important security implications for anyone\nmanaging sensitive data on a NixOS system.</p>\n<p>What Does “World-Readable” Mean?</p>\n<ul>\n<li>\n<p>All files in /nix/store are readable by any user on the system.</p>\n</li>\n<li>\n<p>This is by design, the nix store is intended to be shared, immutable store of\nall packages and configuration files.</p>\n</li>\n<li>\n<p>Permissions are typically set to <code>r-xr-xr-x</code>(read and execute for everyone)</p>\n</li>\n</ul>\n<p><strong>Security Implications</strong></p>\n<ul>\n<li>\n<p>Never store secrets or sensitive data in plane text in the Nix store.</p>\n</li>\n<li>\n<p>If you include secrets directly in your configuration, they will end up in the\n<code>/nix/store</code> and be accessible to any user or process on the system.</p>\n</li>\n<li>\n<p>This applies to files, environment variables, and any data embedded in\nderivations.</p>\n</li>\n</ul>\n<p><strong>Best Practices</strong></p>\n<ul>\n<li>\n<p>Always use a secrets management tool (like <code>sops-nix</code> or <code>agenix</code>) that\ndecrypts secrets at activation time and stores them outside the Nix store,\nwith restricted permissions.</p>\n</li>\n<li>\n<p>Do not embed secrets directly in Nix expressions or configuration files that\nwill be build into the store.</p>\n</li>\n<li>\n<p>Even hashed passwords can be vulnerable when stored in a public repository, be\nconscious of what you store where.</p>\n</li>\n<li>\n<p>If you’re unsure about what’s safe to share, start with a private repository.\nThis gives you time to learn about secrets management and review your\nconfiguration before making anything public.</p>\n</li>\n</ul>\n<p>First, I’ll briefly explain some of the limitations of NixOS Rollbacks and then\nI’ll go into how Git compliments them.</p>\n<h2>Limitations of NixOS Rollbacks</h2>\n<p>NixOS is famous for its ability to roll back to previous system generations,\neither from the boot menu or with commands like <code>nixos-rebuild --rollback</code>.</p>\n<p>When you perform rollbacks in NixOS, whether from the boot menu or using\ncommands like <code>nixos-rebuild --rollback</code> only the contents and symlinks managed\nby the Nix store are affected. The rollback works by switching which system\ngeneration is active, atomically updating symlinks to point to the previous\nversion of all packages, <code>systemd</code> units and services stored in <code>/nix/store</code>.</p>\n<p>However, it’s important to understand what these rollbacks actually do and what\nthey don’t do. What NixOS Rollbacks Cover</p>\n<ul>\n<li>\n<p>System generations: When you rebuild your system, NixOS creates a new\n“generation” that you can boot into or roll back to. This includes all\npackages, services, and system configuration managed by Nix.</p>\n</li>\n<li>\n<p>Quick recovery: If an upgrade breaks your system, you can easily select an\nolder generation at boot and get back to a working state</p>\n</li>\n</ul>\n<p><strong>Key Limitations</strong>:</p>\n<ul>\n<li>\n<p><strong>Configuration files are not reverted</strong>: Rolling back only changes which\nsystem generation is active, it does not revert your actual configuration\nfiles (like <code>configuration.nix</code> or your flake files)</p>\n</li>\n<li>\n<p><strong>User data and service data are not rolled back</strong>: Only files managed by Nix\nare affected. Databases, user files, and other persistent data remain\nunchanged, which can cause problems if, for example, a service migrates its\ndatabase schema during an upgrade</p>\n</li>\n<li>\n<p><strong>Manual changes persist</strong>: Any manual edits to configuration files or system\nstate outside of Nix are not reverted by a rollback</p>\n</li>\n</ul>\n<h2>How Git Helps</h2>\n<!-- ![Git Logo 2](../images/git3.png) -->\n<ul>\n<li>\n<p>The <a href=\"https://docs.github.com/en/github-cli/github-cli/quickstart\">gh-cli</a>,\nsimplifies quite a few things for working with GitHub from the command line.</p>\n</li>\n<li>\n<p><strong>Tracks every configuration change</strong>: By version-controlling your NixOS\nconfigs with Git, you can easily see what changed, when, and why.</p>\n</li>\n<li>\n<p><strong>True config rollback</strong>: If a configuration change causes issues, you can use\n<code>git checkout</code> or <code>git revert</code> to restore your config files to a previous good\nstate, then rebuild your system</p>\n</li>\n<li>\n<p><strong>Safer experimentation</strong>: You can confidently try new settings or upgrades,\nknowing you can roll back both your system state (with NixOS generations) and\nyour config files (with Git).</p>\n</li>\n<li>\n<p><strong>Collaboration and backup</strong>: Git lets you share your setup, collaborate with\nothers, and restore your configuration if your machine is lost or damaged.</p>\n</li>\n</ul>\n<p>In summary: NixOS rollbacks are powerful for system state, but they don’t manage\nyour configuration file history. Git fills this gap, giving you full control and\ntraceability over your NixOS configs making your system both robust and truly\nreproducible. Version control is a fundamental tool for anyone working with\nNixOS, whether you’re customizing your desktop, managing servers, or sharing\nyour configuration with others. Git is the most popular version control system\nand is used by the NixOS community to track, share, and back up system\nconfigurations.</p>\n<p><strong>Why use Git with NixOS?</strong></p>\n<ul>\n<li>\n<p><strong>Track every change</strong>: Git lets you record every modification to your\nconfiguration files, so you can always see what changed, when, and why.</p>\n</li>\n<li>\n<p><strong>Experiment safely</strong>: Try new settings or packages without fear—if something\nbreaks, you can easily roll back to a previous working state.</p>\n</li>\n<li>\n<p><strong>Sync across machines</strong>: With Git, you can keep your NixOS setups in sync\nbetween your laptop, desktop, or servers, and collaborate with others.</p>\n</li>\n<li>\n<p><strong>Disaster recovery</strong>: Accidentally delete your config? With Git, you can\nrestore it from your repository in minutes.</p>\n</li>\n</ul>\n<p>Installing Git on NixOS</p>\n<p>You can install Git by adding it to your system packages in your\nconfiguration.nix or via Home Manager:</p>\n<h2>Git Tips</h2>\n<!-- ![Octocat](../images/octocat.png) -->\n<p>If you develop good git practices on your own repositories it will make it\neasier to contribute with others as well as get help from others.</p>\n<h2>Atomic Commits</h2>\n<p><strong>Atomic commits</strong> are a best practice in Git where each commit represents a\nsingle, focused, and complete change to the codebase. The main characteristics\nof atomic commits are:</p>\n<ul>\n<li>\n<p><strong>One purpose</strong>: Each commit should address only one logical change or task.</p>\n</li>\n<li>\n<p><strong>Complete</strong>: The commit should leave the codebase in a working state.</p>\n</li>\n<li>\n<p><strong>Descriptive</strong>: The commit message should be able to clearly summarize the\nchange in a single sentence.</p>\n</li>\n</ul>\n<p><strong>Why Atomic Commits Matter</strong></p>\n<ul>\n<li>\n<p><strong>Easier debugging</strong>: You can use tools like <code>git bisect</code> to quickly find\nwhich commit introduced a bug, since each commit is isolated.</p>\n</li>\n<li>\n<p><strong>Simpler reverts</strong>: You can revert without affecting unrelated changes.</p>\n</li>\n<li>\n<p><strong>Better collaboration</strong>: Code reviews and merges are more manageable when\nchanges are small and focused.</p>\n</li>\n</ul>\n<p>When you lump together a bunch of changes into a single commit it can lead to\nquite a few undesirable consequences. They make it harder to track down bugs,\nit’s more difficult to revert undesired changes without reverting desired ones,\nmake larger tickets harder to manage.</p>\n<p><strong>Every time a logical component is completed, commit it</strong>. Smaller commits make\nit easier for other devs and yourself to understand the changes and roll them\nback if necessary. This also makes it easier to share your code with others to\nget help when needed and makes merge conflicts less frequent and complex.</p>\n<p><strong>Finish the component, then commit it</strong>: There’s really no reason to commit\nunfinished work, use <code>git stash</code> for unfinished work and <code>git commit</code> for when\nthe logical component is complete. Use common sense and break complex components\ninto logical chunks that can be finished quickly to allow yourself to commit\nmore often.</p>\n<p><strong>Write Good Commit Messages</strong>: Begin with a summary of your changes, add a line\nof whitespace between the summary and the body of your message. Make it clear\nwhy this change was necessary. Use consistent language with generated messages\nfrom commands like <code>git merge</code> which is imperative and present tense\n(<code>&lt;&lt;change&gt;&gt;</code>, not <code>&lt;&lt;changed&gt;&gt;</code> or <code>&lt;&lt;changes&gt;&gt;</code>).</p>\n<h3>Tips for Keeping Commits Atomic with a Linear History</h3>\n<p>Squashing limits the benefits of atomic commits as it combines them all into a\nsingle commit as if you didn’t take the time to write them all out atomically.</p>\n<p>🧠 Why Rebasing Wins for Linear History</p>\n<ul>\n<li>\n<p>No Merge Bubbles: Rebasing avoids those extra merge commits that clutter\n<code>git log --graph</code>. You get a clean, readable timeline.</p>\n</li>\n<li>\n<p>Atomic Commit Integrity: Each commit stands alone and tells a story. Rebasing\npreserves that narrative without diluting it with merge noise.</p>\n</li>\n<li>\n<p>Better Blame &amp; Bisect: Tools like git blame and git bisect work best when\nhistory is linear and logical.</p>\n</li>\n<li>\n<p>Time-Travel Simplicity: Cherry-picking or reverting is easier when commits\naren’t tangled in merge commits.</p>\n</li>\n</ul>\n<p>By default, when you run <code>git pull</code> git merges the commits into your local repo.\nTo change this to a rebase you can set the following:</p>\n<pre><code class=\"language-bash\">git config --global pull.rebase true\ngit config --global rebase.autoStash true\ngit config --global fetch.prune true  # auto delets remote-tracking branches that no longer exist\ngit config --global pull.ff only          # blocks merge pulls\n</code></pre>\n<p>Note: With pull.ff only pulls will fail if they would have had to merge. This\ncould happen if your local branch has diverged from the remote (e.g., someone\npushed new commits and you also committed locally) <code>git pull</code> will throw an\nerror like:</p>\n<pre><code class=\"language-bash\">fatal: Not possible to fast-forward, aborting.\n</code></pre>\n<p><strong>How to fix it</strong></p>\n<p>You basically do what Git won’t auto-do:</p>\n<pre><code class=\"language-bash\">git fetch origin\ngit rebase origin/main\n</code></pre>\n<p>This rewinds your local commits, applies remote commits, and replays yours on\ntop, keeping the history linear.</p>\n<p>If you don’t care about your local changes and want to discard them you can use\nthe following command:</p>\n<pre><code class=\"language-bash\">git reset --hard origin/main\n</code></pre>\n<p>This just makes your branch identical to the remote, no rebase required. This\nprevents rogue merge commits, preserving atomic commits and linear logs.</p>\n<p>You could set an alias for this with:</p>\n<pre><code class=\"language-bash\">git config --global alias.grs '!git fetch origin &amp;&amp; git rebase origin/main'\n</code></pre>\n<p>To check whether a setting is active or now you can use:</p>\n<pre><code class=\"language-bash\">git config --get rebase.autoStash\ntrue\n</code></pre>\n<p>To set these options with home-manager:</p>\n<pre><code class=\"language-nix\"># ... snip ...\n    extraConfig = lib.mkOption {\n      type = lib.types.attrs;\n      default = {\n        commit.gpgsign = true;\n        gpg.format = \"ssh\";\n        user.signingkey = \"/etc/ssh/ssh_host_ed25519_key.pub\";\n        extraConfig = {\n          pull = {\n            rebase = true;\n            ff = \"only\";\n        };\n        };\n        rebase = {\n          autoStash = true; # Auto stashes and unstashes local changes during rebase\n        };\n        fetch = {\n          prune = true; # Automatically deletes remote-tracking branches that no longer exist\n        };\n# ... snip ...\n</code></pre>\n<h2>Time Travel in Git</h2>\n<details>\n<summary> ✔️ Click to Expand Time Travel Section </summary>\n<p><strong>View an old commit</strong>:</p>\n<pre><code class=\"language-bash\">git checkout &lt;commit_hash&gt;\n</code></pre>\n<p>This puts you in a “detached HEAD” state, letting you explore code as it was at\nthat commit. To return, checkout your branch again.</p>\n<p><strong>Go back and keep history (revert)</strong>:</p>\n<pre><code class=\"language-bash\">git revert &lt;commit_hash&gt;\n</code></pre>\n<p><strong>Go back and rewrite history (reset)</strong>:</p>\n<ul>\n<li>Soft reset (keep changes staged):</li>\n</ul>\n<pre><code class=\"language-bash\">git reset --soft &lt;commit_hash&gt;\n</code></pre>\n<ul>\n<li>Mixed reset (keep changes in working directory):</li>\n</ul>\n<pre><code class=\"language-bash\">git reset &lt;commit_hash&gt;\n</code></pre>\n<ul>\n<li>Hard reset (discard all changes after the commit):</li>\n</ul>\n<pre><code class=\"language-bash\">git reset --hard &lt;commit_hash&gt;\n</code></pre>\n<p>Use the above command with caution, it can delete commits from history.</p>\n<ul>\n<li>Relative time travel:</li>\n</ul>\n<pre><code class=\"language-bash\">git reset --hard HEAD@{5.minutes.ago}\n</code></pre>\n<p>or</p>\n<pre><code class=\"language-bash\">git reset --hard HEAD@{yesterday}\n</code></pre>\n<p><strong>Create a branch from the past</strong>:</p>\n<pre><code class=\"language-bash\">git checkout -b &lt;new-brach&gt; &lt;commit_hash&gt;\n</code></pre>\n<p>This starts a new branch from any previous commit, preserving current changes.</p>\n</details>\n<p>Some repositories have guidelines, such as Nixpkgs:</p>\n<details>\n<summary> ✔️ Click to Expand Nixpkgs Commit Conventions </summary>\n<p><strong>Commit conventions</strong></p>\n<ul>\n<li>\n<p>Create a commit for each logical unit.</p>\n</li>\n<li>\n<p>Check for unnecessary whitespace with <code>git diff --check</code> before committing.</p>\n</li>\n<li>\n<p>If you have commits pkg-name: oh, forgot to insert whitespace: squash commits\nin this case. Use <code>git rebase -i</code>. See Squashing Commits for additional\ninformation.</p>\n</li>\n<li>\n<p>For consistency, there should not be a period at the end of the commit\nmessage’s summary line (the first line of the commit message).</p>\n</li>\n<li>\n<p>When adding yourself as maintainer in the same pull request, make a separate\ncommit with the message maintainers: <code>add &lt;handle&gt;</code>. Add the commit before\nthose making changes to the package or module. See Nixpkgs Maintainers for\ndetails.</p>\n<p>Make sure you read about any commit conventions specific to the area you’re\ntouching. See: Commit conventions for changes to <code>pkgs</code>. Commit conventions\nfor changes to <code>lib</code>. Commit conventions for changes to <code>nixos</code>. Commit\nconventions for changes to <code>doc</code>, the Nixpkgs manual.</p>\n</li>\n</ul>\n<p><strong>Writing good commit messages</strong></p>\n<p>In addition to writing properly formatted commit messages, it’s important to\ninclude relevant information so other developers can later understand why a\nchange was made. While this information usually can be found by digging code,\nmailing list/Discourse archives, pull request discussions or upstream changes,\nit may require a lot of work.</p>\n<p>Package version upgrades usually allow for simpler commit messages, including\nattribute name, old and new version, as well as a reference to the relevant\nrelease notes/changelog. Every once in a while a package upgrade requires more\nextensive changes, and that subsequently warrants a more verbose message.</p>\n<p>Pull requests should not be squash merged in order to keep complete commit\nmessages and GPG signatures intact and must not be when the change doesn’t make\nsense as a single commit.</p>\n</details>\n<p>A <strong>Git workflow</strong> is a recipe or recommendation for how to use Git to\naccomplish work in a consistent and productive manner. Having a defined workflow\nlets you leverage Git effectively and consistently. This is especially important\nwhen working on a team.</p>\n<p><strong>Origin</strong> is the <em>default name</em> (alias) for the <strong>remote repository</strong> that your\n<strong>local repository</strong> is connected to, usually the one you cloned from.</p>\n<p><strong>Remote Repositories</strong> are versions of your project that are hosted on the\ninternet or network somewhere.</p>\n<ul>\n<li>\n<p>When you run <code>git push origin main</code>, you’re telling Git to push your changes\nto the remote repo called <code>origin</code>.</p>\n</li>\n<li>\n<p>You can see which URL <code>origin</code> points to with <code>git remote -v</code>.</p>\n</li>\n<li>\n<p>You can have multiple remotes (like <code>origin</code>, <code>upstream</code>, etc.) each pointing\nto a different remote repo. Each of which is generally either read-only or\nread/write for you. Collaborating involves managing these remotes and pushing\nand pulling data to and from them when you need to share work.</p>\n</li>\n</ul>\n<blockquote>\n<p>❗ You can have a remote repo on your local machine. The word “remote” doesn’t\nimply that the repository is somewhere else, only that it’s elsewhere.</p>\n</blockquote>\n<ul>\n<li>The name <code>origin</code> is just a convention, it’s not special. It is automatically\nset when you clone a repo.</li>\n</ul>\n<!-- ![git local remote](../images/git_local-remote.png) -->\n<p><strong>Local</strong> is your local copy of the repository, git tracks the differences\nbetween <strong>local</strong> and <strong>remote</strong> which is a repo hosted elsewhere (e.g., GitHub\nGitLab etc.)</p>\n<p>The <strong>Upstream</strong> in Git typically refers to the original repository from which\nyour local repository or fork was derived. The <strong>Upstream</strong> is the remote repo\nthat serves as the main source of truth, often the original project you forked\nfrom. You typically fetch changes from upstream to update your local repo with\nthe latest updates from the original project, but you don’t push to upstream\nunless you have write access.</p>\n<h3>A Basic Git Workflow</h3>\n<!-- ![Git logo 3](../images/git2.png) -->\n<ol>\n<li>Initialize your Repository:</li>\n</ol>\n<p>If you haven’t already created a Git repo in your NixOS config directory (for\nexample, in your flake or <code>/etc/nixos</code>):</p>\n<pre><code class=\"language-bash\">cd ~/flake\ngit init\ngit add .\ngit commit -m \"Initial commit: NixOS Configuration\"\n</code></pre>\n<p>Taking this initial snapshot with Git is a best practice—it captures the exact\nstate of your working configuration before you make any changes.</p>\n<ul>\n<li>\n<p>The command <code>git add .</code> stages all files in the directory (and its\nsubdirectories) for commit, meaning Git will keep track of them in your\nproject history.</p>\n</li>\n<li>\n<p>The command <code>git commit -m \"message\"</code> then saves a snapshot of these staged\nfiles, along with your descriptive message, into the repository.</p>\n<ul>\n<li>Think of a commit as a “save point” in your project. You can always go back\nto this point if you need to, making it easy to experiment or recover from\nmistakes. This two-step process, staging with <code>git add</code> and saving with\n<code>git commit</code> is at the heart of how Git tracks and manages changes over\ntime.</li>\n</ul>\n</li>\n</ul>\n<!-- ![git commit add](../images/git-add-commit.png) -->\n<ol start=\"2\">\n<li>Make and Track Changes:</li>\n</ol>\n<p>Now that you’ve saved a snapshot of your working configuration, you’re free to\nexperiment and try new things, even if they might break your setup.</p>\n<p>Suppose you want to try a new desktop environment, like Xfce. You edit your\n<code>configuration.nix</code> to add:</p>\n<pre><code class=\"language-nix\">services.xserver.desktopManager.xfce.enable = true;\n</code></pre>\n<p>You run:</p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch # if configuration.nix is in /etc/nixos/\n</code></pre>\n<p>But something goes wrong: the system boots, but your desktop is broken or won’t\nstart. You decide to roll back using the boot menu or:</p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --rollback\n</code></pre>\n<p><strong>What happens?</strong></p>\n<ul>\n<li>\n<p>Your system reverts to the previous working generation in <code>/nix/store</code></p>\n</li>\n<li>\n<p>But: Your <code>configuration.nix</code> file is still changed, it still has the line\nenabling Xfce. If you rebuild again, you’ll get the same broken system,\nbecause your config itself wasn’t rolled back.</p>\n</li>\n</ul>\n<p><strong>How does Git Help on Failure?</strong></p>\n<p>Git gives you quite a few options and ways to inspect what has been done.</p>\n<ul>\n<li>\n<p>Use <code>git status</code> to see what’s changed, and <code>git checkout -- &lt;file&gt;</code> to\nrestore any file to its last committed state.</p>\n</li>\n<li>\n<p>Review your changes with <code>git diff</code> to see exactly what you modified before\ndeciding whether to keep or revert those changes.</p>\n</li>\n<li>\n<p>Reset everything with <code>git reset --hard HEAD</code>, this will discard all local\nchanges and return to your last commit.</p>\n</li>\n</ul>\n<p>With Git you can simply run:</p>\n<pre><code class=\"language-bash\">git checkout HEAD~1 configuration.nix\n# or, if you committed before the change:\ngit revert &lt;commit-hash&gt;\n</code></pre>\n<p>Show the full hash of the latest commit:</p>\n<pre><code class=\"language-bash\">git rev-parse HEAD\nf53fef375d89496c0174e70ce94993d43335098e\n</code></pre>\n<p>Short hash:</p>\n<pre><code class=\"language-bash\">git log --pretty=format:'%h' -n 1\nf53fef3\ngit revert f53fef3\n</code></pre>\n<p>Show a list of Recent commits:</p>\n<pre><code class=\"language-bash\">git log\n# a list of all commits, with hashes, author, date, and message\ngit log --oneline\ngit log --oneline\nf53fef3 (HEAD -&gt; main) thunar\nb34ea22 thunar\n801cbcf thunar\n5e72ba5 sops\n8b67c59 sops\n1a353cb sops\n</code></pre>\n<p>You can copy the commit hash from any of these and use it in commands like\n<code>git checkout &lt;hash&gt;</code> or <code>git revert &lt;hash&gt;</code>.</p>\n<p><strong>Commit successful experiments</strong></p>\n<ul>\n<li>If your changes work, stage, and commit them:</li>\n</ul>\n<pre><code class=\"language-bash\">git add .\n# or more specifically the file you changed or created\ngit add configuration.nix\ngit commit -m \"Describe the new feature or fix\"\n</code></pre>\n<h3>Basic Branching</h3>\n<p>With Git you’re always on a branch and the default branch is <code>master</code>. Many\nchange it to <code>main</code> because of the suggestion Git gives you. I think people are\ntoo easily offended these days, just keep this in mind that <code>main</code> and <code>master</code>\nrefer to the main development branch.</p>\n<p>You can get a listing of your current branches with:</p>\n<pre><code class=\"language-bash\">git branch\n* (no branch)\n  main\n</code></pre>\n<p>The <code>*</code> is next to the current branch and is where the <code>HEAD</code> is currently\npointing. It says <code>(no branch)</code> because I’m currently in detached <code>HEAD</code> where\n<code>HEAD</code> points to no branch. The reason for this is because I’ve been trying out\nJujutsu VCS and that’s JJ’s default setting, a detached <code>HEAD</code>.</p>\n<p>Git actually gives you a warning about working in a detached <code>HEAD</code>:</p>\n<pre><code class=\"language-bash\">You are in 'detached HEAD' state. You can make experimental\nchanges and commit them, and you can discard any commits you make\nin this state without impacting any branch by switching back.\n\nIf you want to create a new branch to retain commits you create,\nyou can do so now (using 'git switch -c &lt;new-branch-name&gt;') or\nlater (using 'git branch &lt;new-branch-name&gt; &lt;commit-id&gt;').\n\nSee 'git help switch' for details.\n</code></pre>\n<p>To attach the <code>HEAD</code> (i.e., have the pointer pointing to a branch), use the\n<code>git checkout</code> command</p>\n<pre><code class=\"language-bash\">git checkout main\nSwitched to branch 'main'\n</code></pre>\n<pre><code class=\"language-bash\">git branch\n* main\n# Ensure that you have the latest \"tip\" from the remote repository `origin`\ngit fetch origin main\nFrom github.com:sayls8/flake\n * branch            main       -&gt; FETCH_HEAD\n</code></pre>\n<p>Although we’re working on our own repo and there is basically no chance of our\nlocal branch diverging from our remote, it’s still good to get in the practice\nof getting everything in sync before merging or rebasing etc.</p>\n<p><code>git fetch</code> doesn’t update <code>main</code>, it just updates your references. To update\n<code>main</code> you would use <code>git pull origin/main</code> or <code>git rebase origin/main</code></p>\n<p>You can inspect your upstream branches with the following command:</p>\n<pre><code class=\"language-bash\">git remote show origin\n* remote origin\n  Fetch URL: git@github.com:saylesss88/flake.git\n  Push  URL: git@github.com:saylesss88/flake.git\n  HEAD branch: main\n  Remote branch:\n    main tracked\n  Local ref configured for 'git push':\n    main pushes to main (fast-forwardable)\n</code></pre>\n<p><code>* branch     main      -&gt; FETCH_HEAD</code>: This line signifies that the <code>main</code>\nbranch from the remote repository (likely <code>origin</code>) was successfully fetched,\nand the commit ID of its current tip (its latest commit) is now stored in your\nlocal <code>FETCH_HEAD</code> reference.</p>\n<p>Now that we know our local <code>main</code> is up to date with our remote <code>origin/main</code> we\ncan safely create a new feature branch:</p>\n<pre><code class=\"language-bash\">git checkout -b feature/prose_wrap\nSwitched to a new branch 'feature/prose_wrap'\n</code></pre>\n<p>Right now the branch <code>feature/prose_wrap</code> is exactly the same as <code>main</code> and we\ncan safely make changes without affecting <code>main</code>. We can try crazy or even\n“dangerous” things and always be able to revert to a working state with\n<code>git checkout main</code>.</p>\n<p>If our crazy idea works out, we can then merge our feature branch into <code>main</code>.</p>\n<p>Ok the feature works, I’ve added and committed the change. Now it’s time to\npoint the <code>HEAD</code> to <code>main</code> and then either merge or rebase the feature branch\ninto <code>main</code>:</p>\n<pre><code class=\"language-bash\">git checkout main\ngit fetch origin main\ngit merge feature/prose_wrap\nUpdating c8bd54c..b281f79\nFast-forward\n home/editors/helix/default.nix | 69 +++++++++++++++++++++++++++++++--------------------------------------\n 1 file changed, 31 insertions(+), 38 deletions(-)\n</code></pre>\n<ul>\n<li>“fast-forward” means that our <code>feature/prose_wrap</code> branch was directly ahead\nof the last commit on <code>main</code>. When you merge one commit with another commit\nthat can be reached by following the first commits history, remember the\nfeature branch is exactly the same as <code>main</code> until I made another commit. If\nthe branches diverged more and the history can’t be followed, Git will perform\na 3-way merge where it creates a new “merge commit” that combines the 2\nchanges.</li>\n</ul>\n<p>If you have a bunch of branches and forget which have been merged yet use:</p>\n<pre><code class=\"language-bash\">git branch --merged\nfeature/prose_wrap\n* main\n# OR to see branches that haven't been merged use:\ngit branch --no-merged\n</code></pre>\n<p>It’s now safe to delete the feature branch:</p>\n<pre><code class=\"language-bash\">git branch -d feature/prose_wrap\nDeleted branch feature/prose_wrap (was b281f79)\n</code></pre>\n<blockquote>\n<p>❗ TIP: If your feature branch has a lot of sloppy commits that won’t be of\nmuch benefit to anyone, squash them first then merge. The workflow would look\nsomething like this:</p>\n<pre><code class=\"language-bash\"> # Make sure you're on the main branch\n git checkout main\n\n # Merge the feature branch with squash\n git merge --squash feature/prose_wrap\n</code></pre>\n<ul>\n<li>This combines all the commits in your branch and adds them to your <code>main</code>\nstaging area, it doesn’t move HEAD or create a merge commit for you. To\napply the changes into one big commit, finalize it with:</li>\n</ul>\n<pre><code class=\"language-bash\"> git commit -m \"Add prose wrapping feature\"\n</code></pre>\n<p>This is often referred to as the “squash commit”.</p>\n</blockquote>\n<p>Branching means to diverge from the main line of development and continue to do\nwork without risking messing up your main branch. There are a few commits on\nyour main branch so to visualize this it would look something like this, image\nis from <a href=\"https://git-scm.com/book/en/v2\">Pro Git</a>:</p>\n<!-- ![Git Branch 1](../images/git-branch3.png) -->\n<h2>Nix flake update example with branches</h2>\n<p>Let’s say you haven’t ran <code>nix flake update</code> in a while and you don’t want to\nintroduce errors to your working configuration. To do so we can first, make sure\nwe don’t lose any changes on our main branch:</p>\n<pre><code class=\"language-bash\">git add .\ngit commit -m \"Staging changes before switching branches\"\n# I always like to make sure the configuration will build before pushing to git\nsudo nixos-rebuild switch --flake .\n# If everything builds and looks correct\ngit push origin main\n</code></pre>\n<p>OR, if you have incomplete changes that you don’t want to commit yet you can\nstash them with <code>git stash</code>:</p>\n<pre><code class=\"language-bash\">git status\nOn branch main\nYour branch is ahead of 'origin/main' by 1 commit.\n  (use \"git push\" to publish your local commits)\n\nChanges not staged for commit:\n  (use \"git add &lt;file&gt;...\" to update what will be committed)\n  (use \"git restore &lt;file&gt;...\" to discard changes in working directory)\n        modified:   home/git.nix\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n</code></pre>\n<p>Now we want to switch branches, without committing the incomplete changes to\n<code>git.nix</code>:</p>\n<pre><code class=\"language-bash\">git stash\nSaved working directory and index state WIP on main: 0e46d6b git: lol alias\n\ngit status\nOn branch main\nYour branch is ahead of 'origin/main' by 1 commit.\n  (use \"git push\" to publish your local commits)\n\nnothing to commit, working tree clean\n</code></pre>\n<blockquote>\n<p>❗ <code>git stash</code> is equivalent to <code>git stash push</code></p>\n</blockquote>\n<p>To see which stashes you have stored, use <code>git sash list</code>:</p>\n<pre><code class=\"language-bash\">git stash list\nstash@{0}: WIP on main: 0e46d6b git: lol alias\n</code></pre>\n<p>To apply the most recent stash:</p>\n<pre><code class=\"language-bash\">git stash apply\ngit add home/git.nix\nOn branch main\nYour branch is ahead of 'origin/main' by 1 commit.\n  (use \"git push\" to publish your local commits)\n\nChanges not staged for commit:\n  (use \"git add &lt;file&gt;...\" to update what will be committed)\n  (use \"git restore &lt;file&gt;...\" to discard changes in working directory)\n        modified:   home/git.nix\n\n# or for multiple stashes\ngit stash apply stash@{2}\n</code></pre>\n<p>Running <code>git stash apply</code> applies the changes that were in your stash but\ndoesn’t automatically restage them, to apply the changes and stage them in one\ncommand:</p>\n<pre><code class=\"language-bash\">git stash apply --index\n</code></pre>\n<p>Now let’s create our branch so we can safely update:</p>\n<pre><code class=\"language-bash\">git checkout -b update-test\nSwitched to a new branch 'update-test'\n</code></pre>\n<p><code>-b</code> is to switch to the branch that was just created</p>\n<p>Some may prefer a more descriptive branch name such as: <code>update/flake-inputs</code>, I\nkept it short for the example. Or if your company uses an issue tracker,\nincluding the ticket number in the branch name can be helpful:\n<code>update/123-flake-inputs</code></p>\n<p>The above command is equivalent to:</p>\n<pre><code class=\"language-bash\">git branch update-test\ngit checkout update-test\n</code></pre>\n<p><del>Now our branches would look something like this, note how both branches\ncurrently point to the same commit:</del> I discovered that Git Book has pretty\nrestrictive licensing and will eventually find a replacement.</p>\n<!-- ![Git Branch 2](../images/git-branch2.png) -->\n<p>Now, lets run our update:</p>\n<pre><code class=\"language-bash\">nix flake update\nsudo nixos-rebuild test --flake .\n# If everything looks ok let's try applying the changes\nsudo nixos-rebuild switch --flake .\n# And if everything looks ok:\ngit add .\ngit commit -m \"feat: Updated all flake inputs\"\ngit push origin update-test\n</code></pre>\n<blockquote>\n<p>❗ This is the same workflow for commiting a PR. After you first fork and\nclone the repo you want to work on, you then create a new feature branch and\npush to that branch on your fork. This allows you to create a PR comparing\nyour changes to their existing configuration.</p>\n</blockquote>\n<p><del>At this point our graph would look similar to the following</del>:</p>\n<!-- ![Git Branch 3](../images/git-branch1.png) -->\n<p>If we are satisfied, we can switch back to our <code>main</code> branch and merge\n<code>update-test</code> into it:</p>\n<pre><code class=\"language-bash\">git checkout main\ngit merge origin/update-test\ngit branch -D update-test\nsudo nixos-rebuild test --flake .\nsudo nixos-rebuild switch --flake .\n</code></pre>\n<p>It’s good practice to delete a branch after you’ve merged and are done with it.</p>\n<h2>Rebasing Branches</h2>\n<p>To combine two seperate branches into one unified history you typically use\n<code>git merge</code> or <code>git rebase</code>.</p>\n<p><code>git merge</code> takes two commit pointers and finds a common base commit between\nthem, it then creates a “merge commit” that combines the changes.</p>\n<p><code>git rebase</code> is used to move a sequence of commits to a new base commit.</p>\n<!-- ![Git rebase](../images/rebase.png) -->\n<h2>Configure Git Declaratively</h2>\n<p>The following example is the <code>git.nix</code> from the hydenix project it shows some\ncustom options and a way to manage everything from a single location:</p>\n<pre><code class=\"language-nix\"># git.nix from hydenix: declarative Git configuration for Home Manager\n{ lib, config, ... }:\n\nlet\n  cfg = config.hydenix.hm.git;\nin\n{\n\n  options.hydenix.hm.git = {\n    enable = lib.mkOption {\n      type = lib.types.bool;\n      default = config.hydenix.hm.enable;\n      description = \"Enable git module\";\n    };\n\n    name = lib.mkOption {\n      type = lib.types.nullOr lib.types.str;\n      default = null;\n      description = \"Git user name\";\n    };\n\n    email = lib.mkOption {\n      type = lib.types.nullOr lib.types.str;\n      default = null;\n      description = \"Git user email\";\n    };\n  };\n\n  config = lib.mkIf cfg.enable {\n\n    programs.git = {\n      enable = true;\n      userName = cfg.name;\n      userEmail = cfg.email;\n      extraConfig = {\n        init.defaultBranch = \"main\";\n        pull.rebase = false;\n      };\n    };\n  };\n}\n</code></pre>\n<blockquote>\n<p>❗ You can easily change the name of the option, everything after <code>config.</code> is\ncustom. So you could change it to for example, <code>config.custom.git</code> and you\nwould enable it with <code>custom.git.enable = true;</code> in your <code>home.nix</code> or\nequivalent.</p>\n</blockquote>\n<p>Then he has a <code>hm/default.nix</code> with the following to enable it.</p>\n<pre><code class=\"language-nix\">#...snip...\n\n # hydenix home-manager options go here\n  hydenix.hm = {\n    #! Important options\n    enable = true;\n      git = {\n        enable = true; # enable git module\n        name = null; # git user name eg \"John Doe\"\n        email = null; # git user email eg \"john.doe@example.com\"\n      };\n    }\n\n    # ... snip ...\n</code></pre>\n<p>You can enable git, and set your git username as well as git email right here.</p>\n<h3>Resources</h3>\n<ul>\n<li>\n<p><a href=\"https://gist.github.com/luismts/495d982e8c5b1a0ced4a57cf3d93cf60\">GitCommitBestPractices</a></p>\n</li>\n<li>\n<p><a href=\"https://git-scm.com/book/en/v2\">ProGit</a></p>\n</li>\n<li>\n<p><a href=\"https://ohshitgit.com/\">Oh shit Git</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-11-30T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/intro_to_nushell_on_NixOS.html",
      "url": "https://saylesss88.github.io/intro_to_nushell_on_NixOS.html",
      "title": "Intro to Nushell",
      "content_html": "<h1>Chapter 12</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/nu.png\" alt=\"Nu\" /></p>\n<h2>Intro to Nushell on NixOS</h2>\n<ul>\n<li>\n<p><strong>TL;DR</strong>:I recently switched default shells from zsh to nushell, this post is\nabout some of the challenges and advantages of using nushell with NixOS.</p>\n</li>\n<li>\n<p>While the average user might not immediately see significant advantages, those\nwho frequently work with structured data formats like JSON, YAML, and CSV –\nsuch as developers interacting with APIs, system administrators managing\nconfigurations, and data professionals – will likely find Nushell’s native\ndata handling and powerful pipeline capabilities a plus. Additionally, users\nwho value a more consistent and safer scripting experience might appreciate\nNushell’s language-first design and features like strong typing.</p>\n</li>\n<li>\n<p>I’ll start with some of the unique build design choices and unique features\nthat I think make Nushell special, then show an example using Nushell to\nmanipulate JSON data. Finally, I will highlight some of the visually appealing\naspects of Nushell and lastly I share some resources for learning more.</p>\n</li>\n</ul>\n<h2>The Good</h2>\n<ul>\n<li>\n<p>Nushell borrows concepts from many shells and languages and is itself both a\nprogramming language and a shell. Because of this, it has its own way of\nworking with files, directories, websites, and more.</p>\n</li>\n<li>\n<p>Nushell is powerful and has many essential commands built directly into the\nshell (“internal” commands) rather than a link to an executable. You can use\nthis set of commands across different operating systems, having this\nconsistency is helpful when creating cross-platform code.</p>\n</li>\n<li>\n<p>When internal Nushell commands (like <code>ls</code>, <code>open</code>, <code>where</code>, <code>get</code>, <code>sort-by</code>,\netc.) produce output, they generally do so in Nushell’s structured data format\n(tables or records). This is the shell’s native way of representing\ninformation.</p>\n</li>\n<li>\n<p>Beyond these foundational strengths, Nushell offers a range of unique features\nthat enhance its functionality and make it particularly well-suited for\ndata-heavy tasks. Here are some highlights that showcase its versatility.</p>\n</li>\n</ul>\n<p><strong>Some Unique Features</strong>:</p>\n<ul>\n<li>\n<p>Besides the built-in commands, Nushell has a\n<a href=\"https://www.nushell.sh/book/standard_library.html\">standard library</a> Nushell\noperates on <em>structured data</em>. You could call it a “data-first” shell and\nprogramming language.</p>\n</li>\n<li>\n<p>Also included, is a full-featured dataframe processing engine using\n<a href=\"https://github.com/pola-rs/polars\">Polars</a> if you want to process large data\nefficiently directly in your shell, check out the\n<a href=\"https://www.nushell.sh/book/dataframes.html\">Dataframes-Docs</a></p>\n</li>\n<li>\n<p><strong>Multi-Line Editing</strong>:</p>\n</li>\n<li>\n<p>When writing a long command you can press Enter to add a newline and move to\nthe next line. For example:</p>\n</li>\n</ul>\n<pre><code class=\"language-nu\">ls            |    # press enter\nwhere name =~ |    # press enter, comments after pipe ok\nget name      |    # press enter\nmv ...$in ./backups/\n</code></pre>\n<ul>\n<li>\n<p>This allows you to cycle through the entire multi-line command using the up\nand down arrow keys and then customize different lines or sections of the\ncommand.</p>\n</li>\n<li>\n<p>You can manually insert a newline using <code>Alt+Enter</code> or <code>Shift+Enter</code>.</p>\n</li>\n<li>\n<p>The <a href=\"https://www.nushell.sh/book/line_editor.html\">Reedline-Editor</a> is\npowerful and provides good <code>vi-mode</code> or <code>emacs</code> support built in.</p>\n</li>\n<li>\n<p>It’s default <code>Ctrl+r</code> history command is nice to work with out of the box.</p>\n</li>\n<li>\n<p>The <a href=\"https://www.nushell.sh/book/explore.html#parameters\">explore</a> command, is\nnu’s version of a table pager, just like <code>less</code> but for table structured data:</p>\n</li>\n</ul>\n<pre><code class=\"language-nu\">$nu | explore --peek\n</code></pre>\n<ul>\n<li>\n<p>With the above command you can navigate with vim keybinds or arrow keys.</p>\n</li>\n<li>\n<p>These features demonstrate Nushell’s user-friendly interface, but what truly\nsets it apart is its underlying design as a structured data scripting\nlanguage. This “language-first” approach powers many of its distinctive\ncapabilities.</p>\n</li>\n</ul>\n<p><img src=\"https://saylesss88.github.io/images/explore.png\" alt=\"explore\" /></p>\n<p><strong>Unique design</strong>:</p>\n<ul>\n<li>\n<p><strong>Fundamentally designed as a structured data scripting language</strong>: and then\nit acts as a shell on top of that foundation. This “language first” approach\nis what gives it many of its distinctive features and makes it a powerful\nscripting language. I reiterate this here because of the implications of this.\nA few of those features are:</p>\n<ul>\n<li>\n<p><strong>Pipelines of structured data</strong>: Unlike traditional shells that primarily\ndeal with plain text streams, Nushell pipelines operate on tables of\nstructured data. Each command can understand and manipulate this structured\ndata directly.</p>\n</li>\n<li>\n<p><strong>Consistent syntax</strong>: Its syntax is more consistent and predictable\ncompared to the often quirky syntax of Bash and Zsh, drawing inspiration\nfrom other programming languages.</p>\n</li>\n<li>\n<p><strong>Strong typing</strong> Nushell has a type system, which helps catch errors early\nand allows for more robust scripting.</p>\n</li>\n<li>\n<p><strong>First-class data types</strong>: It treats various data formats (like JSON, CSV,\nTOML) as native data types, making it easier to work with them. Because of\nthis, Nushell aims to replace the need for external tools like <code>jq</code>, <code>awk</code>,\n<code>sed</code>, <code>cut</code>, and even some uses of <code>grep</code> and <code>curl</code>.</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Variables are Immutable by Default</strong>: Nushell’s commands are based on a\nfunctional-style of programming which requires immutability, sound familiar?</p>\n</li>\n<li>\n<p><strong>Nushell’s Environment is Scoped</strong>: Nushell takes many design cues from\ncompiled languages, one is that languages should avoid global mutable state.\nShells have commonly used global mutation to update the environment, Nushell\nattempts to steer clear of this increasing reproducability.</p>\n</li>\n<li>\n<p><strong>Single-use Environment Variables</strong>:</p>\n</li>\n</ul>\n<pre><code class=\"language-nu\">FOO=BAR $env.FOO\n# =&gt; BAR\n</code></pre>\n<ul>\n<li><strong>Permanent Environment Variables</strong>: In your <code>config.nu</code></li>\n</ul>\n<pre><code class=\"language-nu\"># config.nu\n$env.FOO = 'BAR'\n</code></pre>\n<ul>\n<li>\n<p><a href=\"https://www.nushell.sh/book/coming_from_bash.html\">Coming-From-Bash</a></p>\n</li>\n<li>\n<p>These design principles make Nushell a powerful tool for scripting, but\nthey’re best understood through a hands-on example. Let’s see how Nushell’s\nstructured data capabilities shine in a common task: processing a JSON file.</p>\n</li>\n</ul>\n<p><strong>Example</strong>: I wanted to provide a practical example to illustrate some of these\n“Good” features in action. And break it down for better understanding.</p>\n<ul>\n<li>\n<p>Let’s consider a common task: processing data from a JSON file. Imagine you\nhave a file containing a list of users with their names and ages. With\ntraditional shells, you’d likely need to rely on external tools like <code>jq</code> to\nparse and filter this data. However, Nushell can handle this directly within\nits own commands.</p>\n</li>\n<li>\n<p>For this example you could create a <code>test</code> directory and move to it:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">mkdir test ; cd test\n</code></pre>\n<ul>\n<li>Create a <code>users.json</code> with the following contents:</li>\n</ul>\n<p>👇 users.json</p>\n<pre><code class=\"language-json\">[\n  { \"name\": \"Alice\", \"age\": 25 },\n  { \"name\": \"Bob\", \"age\": 30 },\n  { \"name\": \"Charlie\", \"age\": 20 }\n]\n</code></pre>\n<ul>\n<li>And create the following <code>filter.nu</code> that first converts <code>users.json</code> into its\nown internal structured data format with the <code>open</code> command, then to filters\nout people under <code>21</code> with the <code>where</code> control flow construct, then selects\nthe <code>name</code> and <code>age</code> columns, sorts them by age, and finally converts them\nback to <code>json</code> and saves them to a file called <code>filtered_users.json</code>. A lot\nhappening in a 6 line script.</li>\n</ul>\n<pre><code class=\"language-nu\"># filter.nu\nopen users.json           # Read JSON file into structured data\n| where age &gt; 21         # Filter users older than 21\n| select name age        # Select only name and age columns\n| sort-by age            # Sort by age\n| to json                # Convert back to JSON\n| save filtered_users.json # Save result to a new file\n</code></pre>\n<ul>\n<li>The <code>open</code> command takes data from a file (or even a URL in some cases) and\nparses it and converts it into Nushells own internal structured data format.\nSo this command isn’t just showing you the contents of <code>users.json</code> but doing\na conversion to Nu’s special structured format.</li>\n</ul>\n<pre><code class=\"language-nu\">open users.json\n╭───┬─────────┬─────╮\n│ # │  name   │ age │\n├───┼─────────┼─────┤\n│ 0 │ Alice   │  25 │\n│ 1 │ Bob     │  30 │\n│ 2 │ Charlie │  20 │\n╰───┴─────────┴─────╯\n</code></pre>\n<ul>\n<li>The <code>source</code> command in Nushell is used to execute the commands within a\nscript file (like <code>filter.nu</code>) in the current Nushell environment. It’s\nsimilar to running the script directly in the shell, but keeps the shell open\nfor further use. In this example, <code>source filter.nu</code> runs the commands inside\n<code>filter.nu</code>, processing the <code>users.json</code> file and creating the\n<code>filtered_users.json</code> file:</li>\n</ul>\n<pre><code class=\"language-nu\">source filter.nu\n# View the contents with bat\nbat filtered_users.json\n───────┬──────────────────────────────────────────────────────────────────────────────────────\n       │ File: filtered_users.json\n───────┼──────────────────────────────────────────────────────────────────────────────────────\n   1   │ [\n   2   │   {\n   3   │     \"name\": \"Alice\",\n   4   │     \"age\": 25\n   5   │   },\n   6   │   {\n   7   │     \"name\": \"Bob\",\n   8   │     \"age\": 30\n   9   │   }\n  10   │ ]\n───────┴───────────────────────────────────────────────────────────────────────────────────\n</code></pre>\n<ul>\n<li>As you can see, without needing any external tools, Nushell was able to read,\nfilter, select, sort, and then re-serialize JSON data using a clear and\nconcise pipeline. This demonstrates its power in handling structured data\nnatively, making common data manipulation tasks within the shell significantly\nmore streamlined and readable compared to traditional approaches.</li>\n</ul>\n<p><strong>In the filter.nu example:</strong></p>\n<pre><code class=\"language-nu\"># filter.nu\nopen users.json           # Read JSON file into structured data\n| where age &gt; 21         # Filter users older than 21\n| select name age        # Select only name and age columns\n| sort-by age            # Sort by age\n| to json                # Convert back to JSON\n| save filtered_users.json # Save result to a new file\n</code></pre>\n<details>\n<summary> ✔️ Summary of above Command (Click to Expand)</summary>\n<ol>\n<li>\n<p><code>open users.json</code>: Produces a <strong>Nushell table</strong> representing the data.</p>\n</li>\n<li>\n<p><code>| where age &gt; 21</code>: Receives the table, filters rows based on the <code>age</code>\ncolumn, and outputs a new, filtered table.</p>\n</li>\n<li>\n<p><code>| select name age</code>: Receives the filtered table, selects only the <code>name</code> and\n<code>age</code> columns, and outputs a table with fewer columns.</p>\n</li>\n<li>\n<p><code>| sort-by age</code>: Receives the table, sorts the rows based on the <code>age</code>\ncolumn, and outputs a sorted table.</p>\n</li>\n<li>\n<p><code>| to json</code>: Receives the sorted table and converts it back into JSON text.</p>\n</li>\n<li>\n<p><code>| save filtered_users.json</code>: Receives the JSON text and saves it to a file.</p>\n</li>\n</ol>\n<ul>\n<li>\n<p>So, while the concept of piping is the same, the nature of the data flowing\nthrough the Nushell pipeline is richer and more structured, enabling more\npowerful and direct data manipulation.</p>\n</li>\n<li>\n<p>While Nushell’s strengths, like its structured data pipelines, make it a\ngame-changer for many tasks, it’s not without its challenges, especially when\nintegrated with NixOS’s Bash-centric ecosystem. Let’s explore some of the\nlimitations you might encounter when adopting Nushell as your default shell.</p>\n</li>\n</ul>\n</details>\n<h3>The Bad</h3>\n<ul>\n<li>\n<p>While the project is still maturing, the active community and ongoing\nimprovements are promising. Don’t get too discouraged by the following, there\nwould be a bad section for any shell imo.</p>\n</li>\n<li>\n<p>There are many similarities so it can be easy to forget that some Bash (and\nPOSIX in general) style constructs just won’t work in Nushell. Considering\nthat NixOS seems to have been designed for bash, even Zsh isn’t fully\ncompatable you may want to think twice before you choose Nushell as your\ndefault.</p>\n</li>\n<li>\n<p>The documentation is incomplete and written by devs for devs imo, it is quite\na bit different from anything else I’ve seen so there is a bit of a learning\ncurve. Nushell is generally still considered to be in a stage where it might\nnot be the most seamless or trouble-free experience as a daily driver default\nshell for most users, especially on a system like NixOS known for its unique\napproach.</p>\n</li>\n<li>\n<p>The <a href=\"https://github.com/haslersn/any-nix-shell\">any-nix-shell</a> project doesn’t\ninclude Nushell as with many others because of it’s lack of maturity.</p>\n</li>\n<li>\n<p>The following addition comes from Joey_McKur’s sugggestion, on mentioning the\n<code>job</code> command as one of the biggest criticisms against Nu because it doesn’t\nsupport background tasks. I should also note that Nushell’s team is aware of\nthese criticisms and actively working on improving job control.</p>\n</li>\n</ul>\n<p><strong>Limited Feature Set Compared to Traditional Job Control:</strong></p>\n<ul>\n<li>\n<p><strong>Lack of Full POSIX Job Control</strong>: Nushell’s job control doesn’t yet fully\nimplement all the features and signals defined by POSIX job control (e.g.,\nmore nuanced signal handling, stopped jobs). While it covers the basics, users\naccustomed to advanced Bash job control might find it lacking.</p>\n</li>\n<li>\n<p><strong>Foregrounding Behavior</strong>: There have been criticisms about how foregrounding\njobs interacts with the terminal and potential issues with signal propagation.</p>\n</li>\n</ul>\n<p><strong>Output Handling Challenges</strong>:</p>\n<ul>\n<li>\n<p><strong>Interleaved Output</strong>: Managing the output of multiple backgrounded jobs can\nsometimes be messy, with output from different jobs potentially interleaving\nin the terminal. While Nushell tries to handle this, it’s not always as clean\nas desired.</p>\n</li>\n<li>\n<p><strong>Redirection Complexity</strong>: Redirecting the input and output of backgrounded\njobs can be less straightforward than in Bash, sometimes requiring more\nexplicit handling.</p>\n</li>\n</ul>\n<p><strong>Integration with Pipelines:</strong></p>\n<ul>\n<li><strong>Backgrounding Pipelines</strong>: Backgrounding complex pipelines with multiple\nstages can sometimes lead to unexpected behavior or difficulties in managing\nthe entire pipeline as a single job.</li>\n</ul>\n<p><strong>Error Reporting:</strong></p>\n<ul>\n<li>\n<p><strong>Difficult to Track Errors in Background Jobs</strong>: Identifying and debugging\nerrors in backgrounded jobs can be less direct than with foreground processes,\nand the job command’s output might not always provide sufficient information\nfor troubleshooting.</p>\n</li>\n<li>\n<p>Many of Nushell’s challenges stem from its departure from traditional shell\nconventions, particularly those of Bash, which NixOS heavily relies on. To\nbetter understand these differences and how they impact your workflow, let’s\ncompare Nushell’s static, structured approach to Bash’s dynamic, text-based\nmodel.</p>\n</li>\n</ul>\n<h3>Key Differences Between Nushell &amp; Bash</h3>\n<table><thead><tr><th><strong>Feature</strong></th><th><strong>Bash (Dynamic)</strong></th><th><strong>Nushell (Static)</strong></th></tr></thead><tbody>\n<tr><td>Code Execution</td><td>Line-by-line</td><td>Whole script parsed first</td></tr>\n<tr><td>Error Detection</td><td>Runtime errors only</td><td>Catches errors before running</td></tr>\n<tr><td>Support for <code>eval</code></td><td>✅ Allowed</td><td>❌ Not supported</td></tr>\n<tr><td>Custom Parsing</td><td>Limited</td><td>Built-in semantic analysis</td></tr>\n<tr><td>IDE Features</td><td>Basic syntax highlighting</td><td>Advanced integration, linting, and formatting</td></tr>\n</tbody></table>\n<ul>\n<li>\n<p><code>&amp;&amp;</code> doesn’t work use <code>;</code> instead.</p>\n</li>\n<li>\n<p><code>&gt;</code> is used as the greater-than operator for comparisons:</p>\n</li>\n</ul>\n<pre><code class=\"language-nu\">\"hello\" | save output.txt\n</code></pre>\n<p>is equivalent to the following in bash:</p>\n<pre><code class=\"language-bash\">echo \"hello\" &gt; output.txt\n</code></pre>\n<ul>\n<li>If you notice above the nushell command doesn’t require an <code>echo</code> prefix, this\nis because Nushell has <strong>Implicit Return</strong>:</li>\n</ul>\n<pre><code class=\"language-nu\">\"Hello, World\" == (echo \"Hello, World\")\n# =&gt; true\n</code></pre>\n<ul>\n<li>\n<p>The above example shows that the string, <code>\"Hello, World\"</code> is equivalent to the\noutput value from <code>echo \"Hello, World\"</code></p>\n</li>\n<li>\n<p><strong>Every Command Returns a Value</strong>:</p>\n</li>\n</ul>\n<pre><code class=\"language-nu\">let p = 7\nprint $p  # 7\n$p * 6    # 42\n</code></pre>\n<ul>\n<li>Understanding these differences highlights why Nushell feels so distinct from\nBash, but it’s the shell’s advanced features and integrations that truly make\nit shine. Let’s dive into some of the beautiful and powerful tools and custom\ncommands that elevate Nushell for NixOS users.</li>\n</ul>\n<h3>The Beautiful and Powerful</h3>\n<ul>\n<li>\n<p><code>Ctrl+t</code> List Commands with carapace and fzf:</p>\n<p><img src=\"https://saylesss88.github.io/images/nu4.png\" alt=\"nu4\" /></p>\n</li>\n<li>\n<p><code>Carapace</code>\n<a href=\"https://carapace-sh.github.io/carapace-bin/install.html\">Carapace-Bin Install</a>:</p>\n</li>\n</ul>\n<p>The folling is showing tab completion, I typed <code>hx fl&lt;TAB&gt;</code>:</p>\n<p><img src=\"https://saylesss88.github.io/images/nu9.png\" alt=\"nu9\" /></p>\n<ul>\n<li>\n<p><code>Carapace</code> man example:</p>\n<p><img src=\"https://saylesss88.github.io/images/nu7.png\" alt=\"nu7\" /></p>\n</li>\n</ul>\n<p><strong>Custom Nushell Commands</strong></p>\n<p>Most of the following scripts come from the\n<a href=\"https://github.com/nushell/nu_scripts#\">nu_scripts repo</a></p>\n<ul>\n<li>The following command allows you to choose which input to update interactively\nwith fzf.</li>\n</ul>\n<details>\n<summary> ✔️ Click to See Command</summary>\n<pre><code class=\"language-nu\"># nix.nu\n# upgrade system packages\n# `nix-upgrade` or `nix-upgrade -i`\ndef nix-upgrade [\n  flake_path: string = \"/home/jr/flake\", # path that contains a flake.nix\n  --interactive (-i) # select packages to upgrade interactively\n]: nothing -&gt; nothing {\n  let working_path = $flake_path | path expand\n  if not ($working_path | path exists) {\n    echo \"path does not exist: $working_path\"\n    exit 1\n  }\n  let pwd = $env.PWD\n  cd $working_path\n  if $interactive {\n    let selections = nix flake metadata . --json\n    | from json\n    | get locks.nodes\n    | columns\n    | str join \"\\n\"\n    | fzf --multi --tmux center,20%\n    | lines\n    # Debug: Print selections to verify\n    print $\"Selections: ($selections)\"\n    # Check if selections is empty\n    if ($selections | is-empty) {\n      print \"No selections made.\"\n      cd $pwd\n      return\n    }\n    # Use spread operator to pass list items as separate arguments\n    nix flake update ...$selections\n  } else {\n    nix flake update\n  }\n  cd $pwd\n  nh os switch $working_path\n}\n</code></pre>\n</details>\n<p><strong>Usage</strong>:</p>\n<pre><code class=\"language-nu\">nix-upgrade\n# or for individual packages\nnix-upgrade -i\n</code></pre>\n<p><img src=\"https://saylesss88.github.io/images/nu5.png\" alt=\"nu5\" /></p>\n<ul>\n<li>The <code>ns</code> command is designed to search for Nix packages using <code>nix search</code> and\npresent the results in a cleaner format, specifically removing the\narchitecture and operating system prefix that nix search often includes.</li>\n</ul>\n<details>\n<summary> ✔️ Click To Expand</summary>\n<pre><code class=\"language-nu\">def ns [\n    term: string # Search target.\n] {\n\n    let info = (\n        sysctl -n kernel.arch kernel.ostype\n        | lines\n        | {arch: ($in.0|str downcase), ostype: ($in.1|str downcase)}\n    )\n\n    nix search --json nixpkgs $term\n        | from json\n        | transpose package description\n        | flatten\n        | select package description version\n        | update package {|row| $row.package | str replace $\"legacyPackages.($info.arch)-($info.ostype).\" \"\"}\n}\n</code></pre>\n</details>\n<p><strong>Usage</strong>:</p>\n<pre><code class=\"language-nu\">ns fzf&lt;ENTER&gt;\n</code></pre>\n<p><img src=\"https://saylesss88.github.io/images/nu10.png\" alt=\"nu10\" /></p>\n<ul>\n<li><code>nufetch</code> command:</li>\n</ul>\n<details>\n<summary> ✔️ Click To Expand</summary>\n<pre><code class=\"language-nu\"># `nufetch` `(nufetch).packages`\ndef nufetch [] {\n{\n\"kernel\": $nu.os-info.kernel_version,\n\"nu\": $env.NU_VERSION,\n\"packages\": (ls /etc/profiles/per-user | select name | prepend [[name];\n[\"/run/current-system/sw\"]] | each { insert \"number\" (nix path-info --recursive\n ($in | get name) | lines | length) | insert \"size\" ( nix path-info -S\n ($in | get name) | parse -r '\\s(.*)' | get capture0.0 | into filesize) | update\n \"name\" ($in | get name | parse -r '.*/(.*)' | get capture0.0 | if $in == \"sw\"\n {\"system\"} else {$in}) | rename \"environment\"}),\n\"uptime\": (sys host).uptime\n}\n}\n</code></pre>\n</details>\n<p><img src=\"https://saylesss88.github.io/images/nu1.png\" alt=\"nu1\" /></p>\n<ul>\n<li><code>duf</code> command, I have mine aliased to <code>df</code>:</li>\n</ul>\n<p><img src=\"https://saylesss88.github.io/images/nu8.png\" alt=\"nu8\" /></p>\n<ul>\n<li><code>ps</code> command:</li>\n</ul>\n<p><img src=\"https://saylesss88.github.io/images/ps.png\" alt=\"ps\" /></p>\n<ul>\n<li><code>nix-list-system</code> command lists all installed packages:</li>\n</ul>\n<pre><code class=\"language-nu\"># list all installed packages\ndef nix-list-system []: nothing -&gt; list&lt;string&gt; {\n  ^nix-store -q --references /run/current-system/sw\n  | lines\n  | filter { not ($in | str ends-with 'man') }\n  | each { $in | str replace -r '^[^-]*-' '' }\n  | sort\n}\n</code></pre>\n<p><strong>Usage</strong>:</p>\n<pre><code class=\"language-bash\">nix-list-system\n</code></pre>\n<p><img src=\"https://saylesss88.github.io/images/nu6.png\" alt=\"nu6\" /></p>\n<ul>\n<li>These custom Nushell commands showcase its flexibility, but sometimes you need\nto work around Nushell’s limitations, like compatability with certain NixOS\ntools. This is where <code>just</code> and <code>justfiles</code> come in, simplifying complex\nworkflows and bridging gaps in Nushell’s functionality.</li>\n</ul>\n<h2>Using Just and Justfiles</h2>\n<ul>\n<li>\n<p>The following is my <code>justfile</code> that I keep right next to my <code>flake.nix</code> it\nsimplifies some commands and makes things work that weren’t working with\nnushell for my case, you’ll have to change it to match your configuration.\nIt’s not perfect but works for my use case, take whats useful and leave the\nrest.</p>\n</li>\n<li>\n<p>You’ll first need to install <a href=\"https://github.com/casey/just\">just</a> to make use\nof <code>justfiles</code>.</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\"># nix shell nixpkgs#just nixpkgs#nushell\nset shell := [\"nu\", \"-c\"]\nflake_path := \"/home/jr/flake\"\nhostname := \"magic\"\nhome_manager_output := \"jr@magic\"\n\nutils_nu := absolute_path(\"utils.nu\")\n\ndefault:\n    @just --list\n# Rebuild\n[group('nix')]\nfr:\n    nh os switch --hostname {{hostname}} {{flake_path}}\n\n# Flake Update\n[group('nix')]\nfu:\n    nh os switch  --hostname {{hostname}} --update {{flake_path}}\n\n# Update specific input\n# Usage: just upp nixpkgs\n[group('nix')]\nupp input:\n    nix flake update {{input}}\n# Test\n[group('nix')]\nft:\n    nh os test --hostname {{hostname}} {{flake_path}}\n# Collect Garbage\n[group('nix')]\nncg:\n    nix-collect-garbage --delete-old ; sudo nix-collect-garbage -d ; sudo /run/current-system/bin/switch-to-configuration boot\n\n[group('nix')]\ncleanup:\n    nh clean all\n\n</code></pre>\n<ul>\n<li>To list available commands type, (you must be in the same directory as the\njustfile): <code>just</code></li>\n</ul>\n<p><img src=\"https://saylesss88.github.io/images/just2.png\" alt=\"just\" /></p>\n<ul>\n<li>\n<p>So <code>just fmt</code> will run <code>nix fmt</code>.</p>\n</li>\n<li>\n<p>A lot of the <code>.nu</code> files came from this repo by BlindFS:</p>\n<ul>\n<li>\n<p><a href=\"https://github.com/blindFS/modern-dot-files/tree/main\">modern-dot-files</a> he\nuses Nix Darwin so there are a few changes for NixOS. I found this through\n<a href=\"https://github.com/nushell/this_week_in_nu\">this_week_in_nu</a>.</p>\n</li>\n<li>\n<p><a href=\"https://github.com/TSawyer87/flakes/tree/main/homeManagerModules/shells/nushell\">my-nu-config</a>\nIf you use this, you’ll need to change the first line of <code>fzf.nu</code> to the\nlocation of your config. You’ll also need to change the constants at the top\nof <code>config.nu</code>. These are my old dotfiles, I have recently updated and made\nsure this config is up to date with recent nushell changes. Also, change the\n<code>let flake_path = ($env.HOME | path join \"flake\")</code> to your flake path.</p>\n</li>\n<li>\n<p>The examples use this starship\nconfig<a href=\"https://github.com/Aylur/dotfiles/blob/main/home/starship.nix\">Aylur-dotfiles</a>\nThe logic on the bottom enables starship for Nushell, Zsh, and Bash!</p>\n</li>\n<li>\n<p>If you wan’t to use my config you’ll have to enable the experimental-feature\n<code>pipe-operators</code> in the same place you enable flakes and nix-command.</p>\n</li>\n</ul>\n</li>\n<li>\n<p>There are still situations where I need to switch to zsh or bash to get\nsomething to work i.e. <code>nix-shell</code> and a few others.</p>\n</li>\n<li>\n<p>From custom commands to <code>justfile</code> integrations, Nushell offers a wealth of\ntools to enhance your NixOS experience, even if occasional workarounds are\nneeded. To dive deeper into Nushell and tailor it to your needs, here are some\nvaluable resources to explore, from official documentation to community-driven\nconfigurations.</p>\n</li>\n</ul>\n<h4>Resources</h4>\n<details>\n<summary> ✔️ Click to Expand Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://www.nushell.sh/book/\">Nushell-Book</a></p>\n</li>\n<li>\n<p><a href=\"https://www.nushell.sh/cookbook/\">Nushell-Cookbook</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nushell/nu_scripts\">nu_scripts</a> some of the custom\ncommands came from here.</p>\n</li>\n<li>\n<p><a href=\"https://github.com/nushell/nushell/tree/main/crates/nu-utils/src/default_files\">nushell sample-config</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nushell/awesome-nu#plugins\">awesome-nu repo</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nushell/showcase\">nu showcase-repo</a></p>\n</li>\n<li>\n<p><a href=\"https://discord.com/invite/NtAbbGn\">discord</a> You can find custom commands,\nconfigurations, etc here.</p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2025-11-30T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Intro_to_Nix_Derivations_7.html",
      "url": "https://saylesss88.github.io/Intro_to_Nix_Derivations_7.html",
      "title": "Intro to Derivations",
      "content_html": "<h1>Chapter 7</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<h2>Introduction to Nix Derivations</h2>\n<p><img src=\"https://saylesss88.github.io/images/gruv10.png\" alt=\"gruv10\" /></p>\n<p>Nix’s build instructions, known as <strong>derivations</strong>, are defined using the Nix\nLanguage. These derivations can describe anything from individual software\npackages to complete system configurations. The Nix package manager then\ndeterministically “realizes” (builds) these derivations, ensuring consistency\nbecause they rely solely on a predefined set of inputs.</p>\n<p>Most things in NixOS are built around derivations. Your NixOS system is\ndescribed by such a single system derivation. When you want to apply a new\nconfiguration, <code>nixos-rebuild</code> handles the process:</p>\n<p>It first builds this derivation:</p>\n<pre><code class=\"language-bash\">nix-build '&lt;nixpkgs/nixos&gt;' -A system\n</code></pre>\n<p>Then, once the build is complete, it switches to that new system:</p>\n<pre><code class=\"language-bash\">result/bin/switch-to-configuration\n</code></pre>\n<p>After the build, <code>nixos-rebuild</code> updates a crucial symbolic link:\n<code>/run/current-system</code> This symlink always points to the active, running version\nof your system in the Nix store. In essence, the <code>/run/current-system</code> path is\nthe currently active system derivation. This design choice gives NixOS its\npowerful atomic upgrade and rollback capabilities: changing your system involves\nbuilding a new system derivation and updating this symlink to point to the\nlatest version.</p>\n<blockquote>\n<pre><code class=\"language-nix\"> ls -lsah /run/current-system\n 0 lrwxrwxrwx 1 root root 85 May 23 12:11 /run/current-system -&gt; /nix/store/\n cy2c0kxpjrl7ajlg9v3zh898mhj4dyjv-nixos-system-magic-25.11.20250520.2795c50\n</code></pre>\n</blockquote>\n<ul>\n<li>\n<p>The <code>-&gt;</code> indicates a symlink and it’s pointing to a <strong>store path</strong> which is\nthe result of a derivation being built (the system closure)</p>\n</li>\n<li>\n<p>For beginners, the analogy of a cooking recipe is helpful:</p>\n<ul>\n<li>\n<p><strong>Ingredients (Dependencies):</strong> What other software or libraries are needed.</p>\n</li>\n<li>\n<p><strong>Steps (Build Instructions):</strong> The commands to compile, configure, and\ninstall.</p>\n</li>\n<li>\n<p><strong>Final Dish (Output):</strong> The resulting package or resource.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>A Nix derivation encapsulates all this information, telling Nix what inputs to\nuse, how to build it, and what the final output should be.</p>\n<p>Nix derivations run in <strong>pure</strong>, <strong>isolated environments</strong>, meaning they\n<strong>cannot</strong> access the internet during the build phase. This ensures that builds\nare reproducible – they don’t depend on external sources that might change over\ntime.</p>\n<p>There are <code>Fixed-output-derivations</code> that allow fetching resources during the\nbuild process by explicitly specifying the expected hash upfront. Just keep this\nin mind that normal derivations don’t have network access.</p>\n<h2>Creating Derivations in Nix</h2>\n<p>The primary way to define packages in Nix is through the <code>mkDerivation</code>\nfunction, which is part of the standard environment (<code>stdenv</code>). While a\nlower-level <code>derivation</code> function exists for advanced use cases, <code>mkDerivation</code>\nsimplifies the process by automatically managing dependencies and the build\nenvironment.</p>\n<p><code>mkDerivation</code> (and <code>derivation</code>) takes a set of attributes as its argument. At\na minimum, you’ll often encounter these essential attributes:</p>\n<ol>\n<li>\n<p><strong>name:</strong> A human-readable identifier for the derivation (e.g., “foo”,\n“hello.txt”). This helps you and Nix refer to the package.</p>\n</li>\n<li>\n<p><strong>system:</strong> Specifies the target architecture for the build (e.g.,\n<code>builtins.currentSystem</code> for your current machine).</p>\n</li>\n<li>\n<p><strong>builder:</strong> Defines the program that will execute the build instructions\n(e.g., <code>bash</code>).</p>\n</li>\n</ol>\n<p><strong>How do we pass these required attributes to the <code>derivation</code> function?</strong></p>\n<p>Functions in Nix often take a single argument which is an attribute set. For\n<code>derivation</code> and <code>mkDerivation</code>, this takes the form\n<code>functionName { attribute1 = value1; attribute2 = value2; ... }</code>, where the <code>{}</code>\nencloses the set of attributes being passed as the function’s argument.</p>\n<p>Remember that <code>derivation</code> and <code>mkDerivation</code> take a set (i.e. <code>{}</code>) of\nattributes as its first argument. So, in order to pass the required attributes\nyou would do something like this:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; pkgs = import &lt;nixpkgs&gt; {}\n\nnix-repl&gt; d = derivation {\n            name = \"mydrv\";\n            builder = \"${pkgs.bash}/bin/bash\";\n            args = [\n              \"-c\" # Tells bash to execute the following string as a command\n              ''\n                # Explicitly set PATH to include coreutils bin directory\n                export PATH=\"${pkgs.coreutils}/bin:$PATH\"\n                mkdir $out\n              ''\n            ];\n            system = builtins.currentSystem;\n          }\n\nnix-repl&gt; :b d\n</code></pre>\n<ul>\n<li>When I was starting out, seeing the above written in the following format made\nit clearer in my mental map that we were passing these attributes as arguments\nbut both accomplish the same thing.</li>\n</ul>\n<pre><code class=\"language-nix\">d = derivation { name = \"myname\"; builder = \"${coreutils}/bin/true\"; system = builtins.currentSystem; }\n</code></pre>\n<ul>\n<li>When you write <code>pkgs = import &lt;nixpkgs&gt; {};</code>, you are importing the Nixpkgs\n<code>default.nix</code> file, which resolves to a function. Calling that function by\npassing it an empty attribute set <code>{}</code> as its argument. The function then\nevaluates and returns the entire <code>pkgs</code> attribute set. To specify a different\nsystem for example, you could do something like:</li>\n</ul>\n<pre><code class=\"language-nix\">pkgsForAarch64 = import &lt;nixpkgs&gt; { system = \"aarch64-linux\"; };\n</code></pre>\n<p>So when you see:</p>\n<pre><code class=\"language-nix\">import &lt;nixpkgs&gt; { overlays = []; config = {}; }\n</code></pre>\n<ul>\n<li>\n<p>Instead, these empty sets explicitly override any global or implicit\noverlays/configurations that Nix might otherwise pick up from environment\nvariables (like <code>NIXPKGS_CONFIG</code>), default locations (like\n<code>~/.config/nixpkgs/config.nix</code> or <code>~/.config/nixpkgs/overlays</code>), or other\nmechanisms.</p>\n</li>\n<li>\n<p>This is to prevent accidental partial application from other parts of your\nconfiguration and is saying “Do not pass any custom configuration options for\nthis particular import”</p>\n</li>\n<li>\n<p><code>derivation</code> is a pre-made, built-in function in the Nix language. Here, we\nare passing it an attribute set as argument with the three required\nattributes. (<code>name</code>, <code>builder</code>, <code>system</code>, and we added an extra argument\n<code>args</code>.)</p>\n</li>\n</ul>\n<h2>The Hello World Derivation</h2>\n<p>For this example, first create a <code>hello</code> directory and add the\n<a href=\"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\">Hello tarball</a> to said\ndirectory.</p>\n<p>Now lets create the classic Hello derivation:</p>\n<pre><code class=\"language-nix\"># hello.nix\nlet\n  pkgs = import &lt;nixpkgs&gt; { };\nin\nderivation {\n  name = \"hello\";\n  builder = \"${pkgs.bash}/bin/bash\";\n  args = [ ./hello_builder.sh ];\n  inherit (pkgs)\n    gnutar\n    gzip\n    gnumake\n    gcc\n    coreutils\n    gawk\n    gnused\n    gnugrep\n    ;\n  bintools = pkgs.binutils.bintools;\n  src = ./hello-2.12.1.tar.gz;\n  system = builtins.currentSystem;\n}\n</code></pre>\n<ul>\n<li>As you can see, this isn’t the only required file but is a recipe outlining\nhow to build the <code>hello</code> package. The <code>tar.gz</code> package can be found\n<a href=\"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\">here</a> You would just place\nthe tarball in the same directory as the derivation along with the following\n<code>hello_builder.sh</code>:</li>\n</ul>\n<pre><code class=\"language-bash\"># hello_builder.sh\nexport PATH=\"$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin\"\ntar -xzf $src\ncd hello-2.12.1\n./configure --prefix=$out\nmake\nmake install\n</code></pre>\n<p>And build it with:</p>\n<pre><code class=\"language-bash\">nix-build hello.nix\n</code></pre>\n<p>Finally execute it with:</p>\n<pre><code class=\"language-bash\">./result/bin/hello\nHello, world!\n</code></pre>\n<h2>Simple Rust Derivation</h2>\n<p>Create a <code>simple.rs</code> with the following contents:</p>\n<pre><code class=\"language-rust\">fn main() {\n  println!(\"Simple Rust!\")\n}\n</code></pre>\n<p>And a <code>rust_builder.sh</code> like this (this is our builder script):</p>\n<pre><code class=\"language-bash\"># rust_builder.sh\n# Set up the PATH to include rustc coreutils and gcc\nexport PATH=\"$rustc/bin:$coreutils/bin:$gcc/bin\"\n\n# IMPORTANT: Create the $out directory BEFORE rustc tries to write to it\nmkdir -p \"$out\"\n\n# Compile the Rust source code and place the executable inside $out\nrustc -o \"$out/simple_rust\" \"$src\"\n</code></pre>\n<p>Now we’ll enter the <code>nix repl</code> and build it:</p>\n<pre><code class=\"language-bash\">❯ nix repl\nNix 2.28.3\nType :? for help.\n\nnix-repl&gt; :l &lt;nixpkgs&gt;\nadded 3950 variables.\n\n# Define the variables for rustc, coreutils, bash, AND gcc from the loaded nixpkgs\nnix-repl&gt; rustc = pkgs.rustc\n\nnix-repl&gt; coreutils = pkgs.coreutils\n\nnix-repl&gt; bash = pkgs.bash\n\nnix-repl&gt; gcc = pkgs.gcc\n\n# Now define the derivation\nnix-repl&gt; simple_rust_program = derivation {\n            name = \"simple-rust-program\";\n            builder = \"${bash}/bin/bash\";\n            args = [ ./rust_builder.sh ];\n            rustc = rustc;\n            coreutils = coreutils;\n            gcc = gcc;\n            src = ./simple.rs;\n            system = builtins.currentSystem;\n          }\n\nnix-repl&gt; :b simple_rust_program\nThis derivation produced the following outputs:\nout -&gt; /nix/store/fmyqr2d3ph0lpnxd0xppwvwyhv3iyb7y-simple-rust-program\n</code></pre>\n<pre><code class=\"language-bash\">nix-store -r /nix/store/fmyqr2d3ph0lpnxd0xppwvwyhv3iyb7y-simple-rust-program\n\nwarning: you did not specify '--add-root'; the result might be removed by the garbage collector\n/nix/store/fmyqr2d3ph0lpnxd0xppwvwyhv3iyb7y-simple-rust-program\n</code></pre>\n<p>This simple Rust example, built with a direct derivation call, illustrates:</p>\n<ul>\n<li>\n<p>How Nix explicitly manages every single tool in your build environment\n(<code>bash</code>, <code>rustc</code>, <code>gcc</code>, <code>coreutils</code>).</p>\n</li>\n<li>\n<p>The strict isolation of Nix builds, where nothing is implicitly available.</p>\n</li>\n<li>\n<p>The deterministic mapping of inputs to unique output paths in the Nix store.</p>\n</li>\n<li>\n<p>The above example shows the fundamental structure of a Nix derivation, how\nit’s defined within the <code>nix-repl</code>.</p>\n</li>\n<li>\n<p><code>.drv</code> files are intermediate files that describe how to build a derivation;\nit’s the bare minimum information.</p>\n</li>\n</ul>\n<h2>When Derivations are Built</h2>\n<p>Nix doesn’t build derivations during the evaluation of your Nix expressions.\nInstead, it processes your code in two main phases (and why you need to use\n<code>:b simple_rust_program</code> or <code>nix-store -r</code> to actually build or realize it):</p>\n<ol>\n<li>\n<p>Evaluation/Instantiate Phase: This is when Nix parses and interprets your\n.nix expression. The result is a precise derivation description (often\nrepresented as a .drv file on disk), and the unique “out paths” where the\nfinal built products will go are calculated. No actual code is compiled or\nexecuted yet. Achieved with <code>nix-instantiate</code></p>\n</li>\n<li>\n<p>Realize/Build Phase: Only after a derivation has been fully described does\nNix actually execute its build instructions. It first ensures all the\nderivation’s inputs (dependencies) are built, then runs the builder script\nin an isolated environment, and places the resulting products into their\ndesignated “out paths” in the Nix store. Achieved with <code>nix-store -r</code></p>\n</li>\n</ol>\n<h2>Referring to other derivations</h2>\n<p>The way that we can refer to other packages/derivations is to use the <code>outPath</code>.</p>\n<p>The <code>outPath</code> describes the location of the files of that derivation. Nix can\nthen convert the derivation set into a string:</p>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; :l &lt;nixpkgs&gt;\nnix-repl&gt; fzf\n«derivation /nix/store/vw1zag9q4xvf10z24j1qybji7wfsz78v-fzf-0.62.0.drv»\nnix-repl&gt; fzf.outPath\n\"/nix/store/z3ayhjslz72ldiwrv3mn5n7rs96p2g8a-fzf-0.62.0\"\nnix-repl&gt; builtins.toString fzf\n\"/nix/store/z3ayhjslz72ldiwrv3mn5n7rs96p2g8a-fzf-0.62.0\"\n</code></pre>\n<ul>\n<li>As long as there is an <code>outPath</code> attribute, Nix will do the “set to string\nconversion”.</li>\n</ul>\n<h2>Produce a development shell from a derivation</h2>\n<p>Building on the concept of a derivation as a recipe, let’s create our first\npractical derivation. This example shows how to define a temporary development\nenvironment (a shell) using stdenv.mkDerivation, which is the primary function\nfor defining packages in Nix.</p>\n<pre><code class=\"language-nix\"># my-shell.nix\n# We use a `let` expression to bring `pkgs` and `stdenv` into scope.\n# This is a recommended practice over `with import &lt;nixpkgs&gt; {}`\n# for clarity and to avoid potential name collisions.\nlet\n  pkgs = import &lt;nixpkgs&gt; {};\n  stdenv = pkgs.stdenv; # Access stdenv from the imported nixpkgs\nin\n\n# Make a new \"derivation\" that represents our shell\nstdenv.mkDerivation {\n  name = \"my-environment\";\n\n  # The packages in the `buildInputs` list will be added to the PATH in our shell\n  buildInputs = [\n    # cowsay is an arbitrary package\n    # see https://nixos.org/nixos/packages.html to search for more\n    pkgs.cowsay\n    pkgs.fortune\n  ];\n}\n</code></pre>\n<p><strong>Usage</strong></p>\n<pre><code class=\"language-bash\">nix-shell my-shell.nix\nfortune | cowsay\n _________________________________________\n/ \"Lines that are parallel meet at        \\\n| Infinity!\" Euclid repeatedly, heatedly, |\n| urged.                                  |\n|                                         |\n| Until he died, and so reached that      |\n| vicinity: in it he found that the       |\n| damned things diverged.                 |\n|                                         |\n\\ -- Piet Hein                            /\n -----------------------------------------\n        \\   ^__^\n         \\  (oo)\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||\n</code></pre>\n<ul>\n<li>To exit type: <code>exit</code></li>\n</ul>\n<p>This Nix expression defines a temporary development shell. Let’s break it down:</p>\n<ul>\n<li>\n<p><code>pkgs = import &lt;nixpkgs&gt; {};</code>: Standard way to get access to all the packages\nand helper functions (i.e. <code>nixpkgs.lib</code>)</p>\n</li>\n<li>\n<p><code>stdenv = pkgs.stdenv;</code>: <code>stdenv</code> provides us <code>mkDerivation</code> and is from the\n<code>nixpkgs</code> collection.</p>\n</li>\n<li>\n<p><code>stdenv.mkDerivation { ... };</code>: This is the core function for creating\npackages.</p>\n<ul>\n<li><code>stdenv</code> provides a set of common build tools and conventions.</li>\n</ul>\n</li>\n<li>\n<p><code>mkDerivation</code> takes an attribute set (a collection of key-value pairs) as its\nargument.</p>\n</li>\n<li>\n<p><code>name = \"my-environment\";</code>: This gives your derivation a human-readable name.</p>\n</li>\n<li>\n<p><code>buildInputs = [ pkgs.cowsay ];</code>: This is a list of dependencies that will be\navailable in the build environment of this derivation (or in the <code>PATH</code> if you\nenter the shell created by this derivation). <code>pkgs.cowsay</code> refers to the\n<code>cowsay</code> package from the imported <code>pkgs</code> collection.</p>\n</li>\n</ul>\n<p>The command <code>nix-instantiate --eval my-shell.nix</code> evaluates the Nix expression\nin the file. It does not build the derivation. Instead, it returns the Nix value\nthat the expression evaluates to.</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval my-shell.nix\n</code></pre>\n<p>This value is a structured data type that encapsulates all the attributes (like\n<code>name</code>, <code>system</code>, <code>buildInputs</code>, etc.) required to build the derivation. Your\noutput shows this detailed internal representation of the derivation’s “recipe”\nas understood by Nix. This is useful for debugging and inspecting the\nderivation’s definition.</p>\n<h2>Our Second Derivation: Understanding the Builder</h2>\n<details>\n<summary> Understanding the Builder (Click to Expand) </summary>\n<ul>\n<li>To understand how derivations work, let’s create a very basic example using a\nbash script as our <code>builder</code>.</li>\n</ul>\n<h3>Why a Builder Script?</h3>\n<ul>\n<li>The <code>builder</code> attribute in a derivation tells Nix <em>how</em> to perform the build\nsteps. A simple and common way to define these steps is with a bash script.</li>\n</ul>\n<h3>The Challenge with Shebangs in Nix</h3>\n<ul>\n<li>\n<p>In typical Unix-like systems, you might start a bash script with a shebang\n(<code>#!/bin/bash</code> or <code>#!/usr/bin/env bash</code>) to tell the system how to execute it.\nHowever, in Nix derivations, we generally avoid this.</p>\n</li>\n<li>\n<p><strong>Reason:</strong> Nix builds happen in an isolated environment where the exact path\nto common tools like <code>bash</code> isn’t known beforehand (it resides within the Nix\nstore). Hardcoding a path or relying on the system’s <code>PATH</code> would break Nix’s\nstateless property.</p>\n</li>\n</ul>\n<h3>The Importance of Statelessness in Nix</h3>\n<ul>\n<li>\n<p><strong>Stateful Systems (Traditional):</strong> When you install software traditionally,\nit often modifies the core system environment directly. This can lead to\ndependency conflicts and makes rollbacks difficult.</p>\n</li>\n<li>\n<p><strong>Stateless Systems (Nix):</strong> Nix takes a different approach. When installing a\npackage, it creates a unique, immutable directory in the Nix store. This\nmeans:</p>\n<ul>\n<li>\n<p><strong>No Conflicts:</strong> Different versions of the same package can coexist without\ninterfering with each other.</p>\n</li>\n<li>\n<p><strong>Reliable Rollback:</strong> You can easily switch back to previous versions\nwithout affecting system-wide files.</p>\n</li>\n<li>\n<p><strong>Reproducibility:</strong> Builds are more likely to produce the same result\nacross different machines if they are “pure” (don’t rely on external system\nstate).</p>\n</li>\n</ul>\n</li>\n</ul>\n<h3>The Isolated Nix Build Environment: A Quick Overview</h3>\n<p>When Nix executes a builder script, it sets up a highly controlled and pristine\nenvironment to ensure <strong>reproducibility</strong> and <strong>isolation</strong>. Here’s what\nhappens:</p>\n<ol>\n<li>\n<p><strong>Fresh Start:</strong> Nix creates a temporary, empty directory for the build and\nmakes it the current working directory.</p>\n</li>\n<li>\n<p><strong>Clean Environment:</strong> It completely clears the environment variables from\nyour shell.</p>\n</li>\n<li>\n<p><strong>Controlled Inputs:</strong> Nix then populates the environment with <em>only</em> the\nvariables essential for the build, such as:</p>\n<ul>\n<li>\n<p><code>$NIX_BUILD_TOP</code>: The path to the temporary build directory.</p>\n</li>\n<li>\n<p><code>$PATH</code>: Carefully set to include only the explicit <code>buildInputs</code> you’ve\nspecified, preventing reliance on arbitrary system tools.</p>\n</li>\n<li>\n<p><code>$HOME</code>: Set to <code>/homeless-shelter</code> to prevent programs from reading\nuser-specific configuration files.</p>\n</li>\n<li>\n<p>Variables for each declared output (<code>$out</code>, etc.), indicating where the\nfinal results should be placed in the Nix store.</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Execution &amp; Logging:</strong> The builder script is run with its specified\narguments. All its output (stdout/stderr) is captured in a log.</p>\n</li>\n<li>\n<p><strong>Clean Up &amp; Registration:</strong> If successful, the temporary directory is\nremoved. Nix then scans the build outputs for references to other store\npaths, ensuring all dependencies are correctly tracked for future use and\ngarbage collection. Finally, it normalizes file permissions and timestamps\nin the output for consistent hashing.</p>\n</li>\n</ol>\n<p>This meticulous setup ensures that your builds are independent of the machine\nthey run on and always produce the same result, given the same inputs.</p>\n<h2>Our builder Script</h2>\n<ul>\n<li>For our first derivation, we’ll create a simple <code>builder.sh</code> file in the\ncurrent directory:</li>\n</ul>\n<pre><code class=\"language-bash\"># builder.sh\ndeclare -xp\necho foo &gt; $out\n</code></pre>\n<ul>\n<li>\n<p>The command <code>declare -xp</code> lists exported variables (it’s a bash builtin\nfunction).</p>\n</li>\n<li>\n<p>Nix needs to know where the final built product (the “cake” in our earlier\nanalogy) should be placed. So, during the derivation process, Nix calculates a\nunique output path within the Nix store. This path is then made available to\nour builder script as an environment variable named <code>$out</code>. The <code>.drv</code> file,\nwhich is the recipe, contains instructions for the builder, including setting\nup this <code>$out</code> variable. Our builder script will then put the result of its\nwork (in this case, the “foo” file) into this specific <code>$out</code> directory.</p>\n</li>\n<li>\n<p>As mentioned earlier we need to find the nix store path to the bash\nexecutable, common way to do this is to load Nixpkgs into the repl and check:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">nix-repl&gt; :l &lt;nixpkgs&gt;\nAdded 3950 variables.\nnix-repl&gt; \"${bash}\"\n\"/nix/store/ihmkc7z2wqk3bbipfnlh0yjrlfkkgnv6-bash-4.2-p45\"\n</code></pre>\n<p>So, with this little trick we are able to refer to <code>bin/bash</code> and create our\nderivation:</p>\n<pre><code class=\"language-bash\">nix-repl&gt; d = derivation { name = \"foo\"; builder = \"${bash}/bin/bash\";\n args = [ ./builder.sh ]; system = builtins.currentSystem; }\nnix-repl&gt; :b d\n[1 built, 0.0 MiB DL]\n\nthis derivation produced the following outputs:\n  out -&gt; /nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo\n</code></pre>\n<ul>\n<li>\n<p>The contents of the resulting store path (<code>/nix/store/...-foo</code>) now contain\nthe file <code>foo</code>, as intended. We have successfully built a derivation!</p>\n</li>\n<li>\n<p>Derivations are the primitive that Nix uses to define packages. “Package” is a\nloosely defined term, but a derivation is simply the result of calling\n<code>builtins.derivation</code>.</p>\n</li>\n</ul>\n</details>\n<h2>Our Last Derivation</h2>\n<p>Create a new directory and a <code>hello.nix</code> with the following contents:</p>\n<pre><code class=\"language-nix\"># hello.nix\n{\n  stdenv,\n  fetchzip,\n}:\n\nstdenv.mkDerivation {\n  pname = \"hello\";\n  version = \"2.12.1\";\n\n  src = fetchzip {\n    url = \"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\";\n    sha256 = \"\";\n  };\n}\n</code></pre>\n<p>Save this file to <code>hello.nix</code> and run <code>nix-build</code> to observe the build failure:</p>\n<ul>\n<li>Click to expand output:</li>\n</ul>\n<pre><code class=\"language-nix\">$ nix-build hello.nix\n~error: cannot evaluate a function that has an argument without a value ('stdenv')\n~       Nix attempted to evaluate a function as a top level expression; in\n~       this case it must have its arguments supplied either by default\n~       values, or passed explicitly with '--arg' or '--argstr'. See\n~       https://nix.dev/manual/nix/stable/language/constructs.html#functions.\n~\n~       at /home/nix-user/hello.nix:3:3:\n~\n~            2| {\n~            3|   stdenv,\n~             |   ^\n~            4|   fetchzip,\n</code></pre>\n<p><strong>Problem</strong>: The expression in <code>hello.nix</code> is a <em>function</em>, which only produces\nit’s intended output if it is passed the correct <em>arguments</em>.(i.e. <code>stdenv</code> is\navailable from <code>nixpkgs</code> so we need to import <code>nixpkgs</code> before we can use\n<code>stdenv</code>):</p>\n<p>The recommended way to do this is to create a <code>default.nix</code> file in the same\ndirectory as the <code>hello.nix</code> with the following contents:</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  nixpkgs = fetchTarball \"https://github.com/NixOS/nixpkgs/tarball/nixos-24.05\";\n  pkgs = import nixpkgs { config = {}; overlays = []; };\nin\n{\n  hello = pkgs.callPackage ./hello.nix { };\n}\n</code></pre>\n<p>This allows you to run <code>nix-build -A hello</code> to realize the derivation in\n<code>hello.nix</code>, similar to the current convention used in Nixpkgs:</p>\n<ul>\n<li>Click to expand Output:</li>\n</ul>\n<pre><code class=\"language-nix\">nix-build -A hello\n~error: hash mismatch in fixed-output derivation '/nix/store/pd2kiyfa0c06giparlhd1k31bvllypbb-source.drv':\n~         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n~            got:    sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=\n~error: 1 dependencies of derivation '/nix/store/b4mjwlv73nmiqgkdabsdjc4zq9gnma1l-hello-2.12.1.drv' failed to build\n</code></pre>\n<ul>\n<li>Another way to do this is with\n<a href=\"https://nix.dev/manual/nix/2.24/command-ref/nix-prefetch-url\">nix-prefetch-url</a>\nIt is a utility to calculate the sha beforehand.</li>\n</ul>\n<pre><code class=\"language-bash\">nix-prefetch-url https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\npath is '/nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz'\n086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd\n</code></pre>\n<ul>\n<li>When you use <code>nix-prefetch-url</code>, you get a Base32 hash when nix needs SRI\nformat.</li>\n</ul>\n<p>Run the following command to convert from Base32 to SRI:</p>\n<pre><code class=\"language-bash\">nix hash to-sri --type sha256 086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd\nsha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=\n</code></pre>\n<ul>\n<li>This actually fetched a different sha than the Nix compiler returned in the\nexample where we replace the empty sha with the one Nix gives us. The\ndifference was that <code>fetchzip</code> automatically extracts archives before\ncomputing the hash and slight differences in the metadata cause different\nresults. I had to switch from <code>fetchzip</code> to <code>fetchurl</code> to get the correct\nresults.\n<ul>\n<li>\n<p>Extracted archives can differ in timestamps, permissions, or compression\ndetails, causing different hash values.</p>\n</li>\n<li>\n<p>A simple takeaway is to use <code>fetchurl</code> when you need an exact match, and\n<code>fetchzip</code> when working with extracted contents.</p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/manual/nixpkgs/stable/#fetchurl\">fetchurl</a></p>\n</li>\n<li>\n<p><code>fetchurl</code> returns a <code>fixed-output derivation</code>(FOD): A derivation where a\ncryptographic hash of the output is determined in advance using the\noutputHash attribute, and where the builder executable has access to the\nnetwork.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>Lastly replace the empty sha256 placeholder with the returned value from the\nlast command:</p>\n<pre><code class=\"language-nix\"># hello.nix\n{\n  stdenv,\n  fetchzip,\n}:\n\nstdenv.mkDerivation {\n  pname = \"hello\";\n  version = \"2.12.1\";\n\n  src = fetchzip {\n    url = \"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\";\n    sha256 = \"sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=\";\n  };\n}\n</code></pre>\n<p>Run <code>nix-build -A hello</code> again and you’ll see the derivation successfully\nbuilds.</p>\n<h2>Best Practices</h2>\n<p><strong>Reproducible source paths</strong>: If we built the following derivation in\n<code>/home/myuser/myproject</code> then the store path of <code>src</code> will be\n<code>/nix/store/&lt;hash&gt;-myproject</code> causing the build to no longer be reproducible:</p>\n<pre><code class=\"language-nix\">let pkgs = import &lt;nixpkgs&gt; {}; in\n\npkgs.stdenv.mkDerivation {\n  name = \"foo\";\n  src = ./.;\n}\n</code></pre>\n<blockquote>\n<p>❗ TIP: Use <code>builtins.path</code> with the <code>name</code> attribute set to something fixed.\nThis will derive the symbolic name of the store path from the <code>name</code> instead\nof the working directory:</p>\n<pre><code class=\"language-nix\">let pkgs = import &lt;nixpkgs&gt; {}; in\n\npkgs.stdenv.mkDerivation {\n  name = \"foo\";\n  src = builtins.path { path = ./.; name = \"myproject\"; };\n}\n</code></pre>\n</blockquote>\n<h3>Conclusion</h3>\n<p>In this chapter, we’ve laid the groundwork for understanding Nix derivations,\nthe fundamental recipes that define how software and other artifacts are built\nwithin the Nix ecosystem. We’ve explored their key components – inputs, builder,\nbuild phases, and outputs – and how they contribute to Nix’s core principles of\nreproducibility and isolated environments. Derivations are the workhorses behind\nthe packages and tools we use daily in Nix.</p>\n<p>As you’ve learned, derivations offer a powerful and principled approach to\nsoftware management. However, the way we organize and manage these derivations,\nalong with other Nix expressions and dependencies, has evolved over time.\nTraditionally, Nix projects often relied on patterns involving <code>default.nix</code>\nfiles, channel subscriptions, and manual dependency management.</p>\n<p>A more recent and increasingly popular approach to structuring Nix projects and\nmanaging dependencies is through Nix Flakes. Flakes introduce a standardized\nproject structure, explicit input tracking, and a more robust way to ensure\nreproducible builds across different environments.</p>\n<p>In our next chapter,\n<a href=\"https://saylesss88.github.io/Comparing_Flakes_and_Traditional_Nix_8.html\">Comparing Flakes and Traditional Nix</a>,\nwe will directly compare and contrast these two approaches. We’ll examine the\nstrengths and weaknesses of traditional Nix practices in contrast to the\nbenefits and features offered by Nix Flakes. This comparison will help you\nunderstand the motivations behind Flakes and when you might choose one approach\nover the other for your Nix projects.</p>\n<p>As you can see below, there is a ton of information on derivations freely\navailable.</p>\n<h4>Links To Articles about Derivations</h4>\n<details>\n<summary> Click To Expand Resources </summary>\n<ul>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/06-our-first-derivation\">NixPillsOurFirstDerivation</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/07-working-derivation\">NixPills-WorkingDerivation</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.24/language/derivations\">nix.dev-Derivations</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/tutorials/packaging-existing-software\">nix.dev-packagingExistingSoftware</a></p>\n</li>\n<li>\n<p><a href=\"https://ianthehenry.com/posts/how-to-learn-nix/my-first-derivation/\">howToLearnNix-MyFirstDerivation</a></p>\n</li>\n<li>\n<p><a href=\"https://ianthehenry.com/posts/how-to-learn-nix/derivations-in-detail/\">howToLearnNix-DerivationsInDetail</a></p>\n</li>\n<li>\n<p><a href=\"https://www.sam.today/blog/creating-a-super-simple-derivation-learning-nix-pt-3\">Sparky/blog-creatingASuperSimpleDerivation</a> #\nHow to learn Nix</p>\n</li>\n<li>\n<p><a href=\"https://www.sam.today/blog/derivations-102-learning-nix-pt-4\">Sparky/blog-Derivations102</a></p>\n</li>\n<li>\n<p><a href=\"https://scrive.github.io/nix-workshop/04-derivations/01-derivation-basics.html\">ScriveNixWorkshop-nixDerivationBasics</a></p>\n</li>\n<li>\n<p><a href=\"https://zero-to-nix.com/concepts/derivations/\">zeroToNix-Derivations</a></p>\n</li>\n<li>\n<p><a href=\"https://www.tweag.io/blog/2021-02-17-derivation-outputs-and-output-paths/\">Tweag-derivationOutputs</a></p>\n</li>\n<li>\n<p><a href=\"https://ayats.org/blog/nix-tuto-2\">theNixLectures-Derivations</a></p>\n</li>\n<li>\n<p><a href=\"https://bmcgee.ie/posts/2023/02/nix-what-are-fixed-output-derivations-and-why-use-them/\">bmcgee-whatAreFixed-OutputDerivations</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2025-11-29T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/flake_inputs_4.1.html",
      "url": "https://saylesss88.github.io/flakes/flake_inputs_4.1.html",
      "title": "Flake Inputs",
      "content_html": "<h1>Nix Flake Inputs</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>The attribute <code>inputs</code> specifies the dependencies of a flake, as an attrset\nmapping input names to flake references.</p>\n<p>If a repository provides a <code>flake.nix</code> you can include it as an input in your\n<code>flake.nix</code>.</p>\n<p>For example, I like yazi as my file explorer and have been using helix as my\neditor. To be able to get yazi to work with helix I needed the latest versions\nof both yazi and helix. One way to get the latest versions was to add their\nflakes as inputs to my flake:</p>\n<pre><code class=\"language-nix\">{\n\tinputs = {\n\t\tnixpkgs.url = \"github:NixOS/nixpkgs/nixos-24.11\";\n\t\thome-manager = {\n\t\t\turl = \"github:nix-community/home-manager/release-24.11\";\n\t\t\tinputs.nixpkgs.follows = \"nixpkgs\";\n\t\t};\n    helix = {\n      url = \"github:helix-editor/helix\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n\t\tyazi.url = \"github:sxyazi/yazi\";\n\t};\n\toutputs = { nixpkgs, home-manager, ... } @ inputs: {\n\t# ... snip ... #\n</code></pre>\n<ul>\n<li>Now to use this input, I would reference these inputs in both my yazi and\nhelix modules:</li>\n</ul>\n<pre><code class=\"language-nix\"># yazi.nix\n{ pkgs, config, inputs, ... }: {\n\tprograms.yazi = {\n\t\tenable = true;\n\t\tpackage = inputs.yazi.packages.${pkgs.system}.default;\n\t};\n}\n</code></pre>\n<pre><code class=\"language-nix\"># helix.nix\n{ pkgs, config, inputs, ... }: {\n\tprograms.helix = {\n\t\tenable = true;\n\t\tpackage = inputs.helix.packages.${pkgs.system}.helix;\n\t};\n}\n</code></pre>\n<p>Understanding <code>.default</code> vs. Named Outputs (e.g., <code>.helix</code>) from the Source</p>\n<p>The difference between <code>inputs.yazi.packages.${pkgs.system}.default</code> and\n<code>inputs.helix.packages.${pkgs.system}.helix</code> comes down to how the respective\nupstream flakes define their outputs. You can always inspect a flake’s\n<code>flake.nix</code> or use <code>nix flake show &lt;flake-reference&gt;</code> to understand its\nstructure.</p>\n<h2>Helix <code>flake.nix</code></h2>\n<p>Let’s look at the relevant section of Helix’s <code>flake.nix</code> click the eye to see\nthe full flake:</p>\n<pre><code class=\"language-nix\">~ {\n~   description = \"A post-modern text editor.\";\n~\n~   inputs = {\n~     nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n~     rust-overlay = {\n~       url = \"github:oxalica/rust-overlay\";\n~       inputs.nixpkgs.follows = \"nixpkgs\";\n~     };\n~   };\n~\n~   outputs = {\n~     self,\n~     nixpkgs,\n~     rust-overlay,\n~     ...\n~   }: let\n~     inherit (nixpkgs) lib;\n~     systems = [\n~       \"x86_64-linux\"\n~       \"aarch64-linux\"\n~       \"x86_64-darwin\"\n~       \"aarch64-darwin\"\n~     ];\n~     eachSystem = lib.genAttrs systems;\n~     pkgsFor = eachSystem (system:\n~       import nixpkgs {\n~         localSystem.system = system;\n~         overlays = [(import rust-overlay) self.overlays.helix];\n~       });\n~     gitRev = self.rev or self.dirtyRev or null;\n   in {\n     packages = eachSystem (system: {\n       inherit (pkgsFor.${system}) helix;\n       /*\n       The default Helix build. Uses the latest stable Rust toolchain, and unstable\n       nixpkgs.\n\n       The build inputs can be overridden with the following:\n\n       packages.${system}.default.override { rustPlatform = newPlatform; };\n\n       Overriding a derivation attribute can be done as well:\n\n       packages.${system}.default.overrideAttrs { buildType = \"debug\"; };\n       */\n      default = self.packages.${system}.helix;\n    });\n~    checks =\n~      lib.mapAttrs (system: pkgs: let\n~        # Get Helix's MSRV toolchain to build with by default.\n~        msrvToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;\n~        msrvPlatform = pkgs.makeRustPlatform {\n~          cargo = msrvToolchain;\n~          rustc = msrvToolchain;\n~        };\n~      in {\n~        helix = self.packages.${system}.helix.override {\n~          rustPlatform = msrvPlatform;\n~        };\n~      })\n~      pkgsFor;\n~\n~    # Devshell behavior is preserved.\n~    devShells =\n~      lib.mapAttrs (system: pkgs: {\n~        default = let\n~          commonRustFlagsEnv = \"-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable\";\n~          platformRustFlagsEnv = lib.optionalString pkgs.stdenv.isLinux \"-Clink-arg=-Wl,--no-rosegment\";\n~        in\n~          pkgs.mkShell {\n~            inputsFrom = [self.checks.${system}.helix];\n~            nativeBuildInputs = with pkgs;\n~              [\n~                lld\n~                cargo-flamegraph\n~                rust-bin.nightly.latest.rust-analyzer\n~              ]\n~              ++ (lib.optional (stdenv.isx86_64 &amp;&amp; stdenv.isLinux) cargo-tarpaulin)\n~              ++ (lib.optional stdenv.isLinux lldb)\n~              ++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation);\n~            shellHook = ''\n~              export RUST_BACKTRACE=\"1\"\n~              export RUSTFLAGS=\"''${RUSTFLAGS:-\"\"} ${commonRustFlagsEnv} ${platformRustFlagsEnv}\"\n~            '';\n~          };\n~      })\n~      pkgsFor;\n~\n~    overlays = {\n~      helix = final: prev: {\n~        helix = final.callPackage ./default.nix {inherit gitRev;};\n~      };\n~\n~      default = self.overlays.helix;\n~    };\n~  };\n~  nixConfig = {\n~    extra-substituters = [\"https://helix.cachix.org\"];\n~    extra-trusted-public-keys = [\"helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs=\"];\n~  };\n~}\n</code></pre>\n<p>Dissecting <code>inherit (pkgsFor.${system}) helix;</code></p>\n<p>Imagine the Nix evaluation process for Helix <code>flake.nix</code> in the <code>outputs</code>\nsection:</p>\n<ol>\n<li>\n<p><code>packages = eachSystem (system: { ... });</code> Part iterates through each\n<code>system</code> (like <code>x86_64-linux</code>). For each <code>system</code>, it’s creating an attribute\nset that will become <code>self.packages.${system}</code>.</p>\n</li>\n<li>\n<p>Inside the <code>eachSystem</code> function, for a specific system (e.g.\n<code>x86_64-linux</code>): The code is building an attribute set that will ultimately\nbe assigned to <code>self.packages.x86_64-linux</code>.</p>\n</li>\n<li>\n<p>When you write <code>inherit (sourceAttrset) attributeName;</code>, it’s equivalent to\nwriting <code>attributeName = sourceAttrset.attributeName;</code>.</p>\n</li>\n</ol>\n<p>So, <code>inherit (pkgsFor.${system}) helix;</code> is equivalent to:</p>\n<pre><code class=\"language-nix\">helix = pkgsFor.${system}.helix;\n</code></pre>\n<p>Therefore, because of <code>inherit (pkgsFor.${system}) helix;</code>, the helix attribute\nis explicitly defined under\n<code>packages.${system}``. This is why you access it as </code>inputs.helix.packages.${pkgs.system}.helix;`.</p>\n<h2>Yazi <code>flake.nix</code></h2>\n<p>Now this is yazi’s <code>flake.nix</code>, yazi’s documentation tells you to use <code>.default</code>\nbut lets examine the flake and see why:</p>\n<pre><code class=\"language-nix\">~{\n~  inputs = {\n~    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n~    flake-utils.url = \"github:numtide/flake-utils\";\n~    rust-overlay = {\n~      url = \"github:oxalica/rust-overlay\";\n~      inputs.nixpkgs.follows = \"nixpkgs\";\n~    };\n~  };\n~\n~  outputs =\n~    {\n~      self,\n~      nixpkgs,\n~      rust-overlay,\n~      flake-utils,\n~      ...\n~    }:\n~    flake-utils.lib.eachDefaultSystem (\n~      system:\n~      let\n~        pkgs = import nixpkgs {\n~          inherit system;\n~          overlays = [ rust-overlay.overlays.default ];\n~        };\n~        toolchain = pkgs.rust-bin.stable.latest.default;\n~        rustPlatform = pkgs.makeRustPlatform {\n~          cargo = toolchain;\n~          rustc = toolchain;\n~        };\n~\n~        rev = self.shortRev or self.dirtyShortRev or \"dirty\";\n~        date = self.lastModifiedDate or self.lastModified or \"19700101\";\n~        version =\n~          (builtins.fromTOML (builtins.readFile ./yazi-fm/Cargo.toml)).package.version\n~          + \"pre${builtins.substring 0 8 date}_${rev}\";\n~      in\n      {\n        packages = {\n          yazi-unwrapped = pkgs.callPackage ./nix/yazi-unwrapped.nix {\n            inherit\n              version\n              rev\n              date\n              rustPlatform\n              ;\n          };\n          yazi = pkgs.callPackage ./nix/yazi.nix { inherit (self.packages.${system}) yazi-unwrapped; };\n          default = self.packages.${system}.yazi;\n        };\n\n~        devShells = {\n~          default = pkgs.callPackage ./nix/shell.nix { };\n~        };\n~\n~        formatter = pkgs.nixfmt-rfc-style;\n~      }\n~    )\n~    // {\n~      overlays = {\n~        default = self.overlays.yazi;\n~        yazi = _: prev: { inherit (self.packages.${prev.stdenv.system}) yazi yazi-unwrapped; };\n~      };\n~    };\n~}\n</code></pre>\n<p>In this case using <code>inputs.yazi.packages.${pkgs.system}.yazi</code> would also work</p>\n<ul>\n<li>\n<p><code>yazi = pkgs.callPackage ./nix/yazi.nix { inherit (self.packages.${system}) yazi-unwrapped; };</code>\nThis line defines the yazi variable (or, more precisely, creates an attribute\nnamed yazi within the <code>packages.${system}</code> set). It assigns to this yazi\nattribute the result of calling the Nix expression in <code>./nix/yazi.nix</code> with\nyazi-unwrapped as an argument. This yazi attribute represents the actual,\nrunnable Yazi package.</p>\n</li>\n<li>\n<p><code>default = self.packages.${system}.yazi;</code> This line then aliases the yazi\npackage. It creates another attribute named <code>default</code> within the same\n<code>packages.${system}</code> set and points it directly to the yazi attribute that was\njust defined.</p>\n</li>\n<li>\n<p>So, when you access <code>inputs.yazi.packages.${pkgs.system}.default</code>, you’re\neffectively following the alias to the yazi package.</p>\n</li>\n<li>\n<p>The choice to use <code>.default</code> is primarily for convenience and adherence to a\ncommon flake convention, making the flake easier for users to consume without\nneeding to dive into its internal structure.</p>\n</li>\n</ul>\n",
      "date_published": "2025-11-28T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/flake_outputs_4.2.html",
      "url": "https://saylesss88.github.io/flakes/flake_outputs_4.2.html",
      "title": "Flake outputs",
      "content_html": "<h1>Nix Flake Outputs</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>Flake outputs are what the flake produces when built. Flakes can have multiple\noutputs simultaneously such as:</p>\n<ul>\n<li>\n<p><strong>Packages</strong>: Self-contained bundles that are built using derivations and\nprovide either some kind of software or dependencies of software.</p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/NixOS_Modules_Explained_3.html\">NixOS modules</a></p>\n</li>\n<li>\n<p>Nix development environments</p>\n</li>\n<li>\n<p><a href=\"https://github.com/NixOS/templates\">Nix templates</a></p>\n</li>\n<li>\n<p>The <code>outputs</code> top-level attribute is actually a function that takes an\nattribute set of inputs and returns an attribute set that is essentially a\nrecipe for building the flake.</p>\n</li>\n</ul>\n<h2>Output Schema</h2>\n<p>Once the inputs are resolved, they’re passed to the <code>outputs</code> attribute. This\n<code>outputs</code> attribute is, in fact, a function, as indicated by the <code>:</code> colon (or\nthe <code>lambda</code> syntax) that follows its definition. This function takes the\nresolved inputs (and <code>self</code>, the flake’s directory in the store) as arguments,\nand its return value dictates the outputs of the flake, following this schema:</p>\n<pre><code class=\"language-nix\">{ self, nixpkgs, ... }@inputs:\n{\n  # Executed by `nix flake check`\n  checks.\"&lt;system&gt;\".\"&lt;name&gt;\" = derivation;\n  # Executed by `nix build .#&lt;name&gt;`\n  packages.\"&lt;system&gt;\".\"&lt;name&gt;\" = derivation;\n  # Executed by `nix build .`\n  packages.\"&lt;system&gt;\".default = derivation;\n  # Executed by `nix run .#&lt;name&gt;`\n  apps.\"&lt;system&gt;\".\"&lt;name&gt;\" = {\n    type = \"app\";\n    program = \"&lt;store-path&gt;\";\n  };\n  # Executed by `nix run . -- &lt;args?&gt;`\n  apps.\"&lt;system&gt;\".default = { type = \"app\"; program = \"...\"; };\n\n  # Formatter (alejandra, nixfmt or nixpkgs-fmt)\n  formatter.\"&lt;system&gt;\" = derivation;\n  # Used for nixpkgs packages, also accessible via `nix build .#&lt;name&gt;`\n  legacyPackages.\"&lt;system&gt;\".\"&lt;name&gt;\" = derivation;\n  # Overlay, consumed by other flakes\n  overlays.\"&lt;name&gt;\" = final: prev: { };\n  # Default overlay\n  overlays.default = final: prev: { };\n  # Nixos module, consumed by other flakes\n  nixosModules.\"&lt;name&gt;\" = { config, ... }: { options = {}; config = {}; };\n  # Default module\n  nixosModules.default = { config, ... }: { options = {}; config = {}; };\n  # Used with `nixos-rebuild switch --flake .#&lt;hostname&gt;`\n  # nixosConfigurations.\"&lt;hostname&gt;\".config.system.build.toplevel must be a derivation\n  nixosConfigurations.\"&lt;hostname&gt;\" = {};\n  # Used by `nix develop .#&lt;name&gt;`\n  devShells.\"&lt;system&gt;\".\"&lt;name&gt;\" = derivation;\n  # Used by `nix develop`\n  devShells.\"&lt;system&gt;\".default = derivation;\n  # Hydra build jobs\n  hydraJobs.\"&lt;attr&gt;\".\"&lt;system&gt;\" = derivation;\n  # Used by `nix flake init -t &lt;flake&gt;#&lt;name&gt;`\n  templates.\"&lt;name&gt;\" = {\n    path = \"&lt;store-path&gt;\";\n    description = \"template description goes here?\";\n  };\n  # Used by `nix flake init -t &lt;flake&gt;`\n  templates.default = { path = \"&lt;store-path&gt;\"; description = \"\"; };\n}\n</code></pre>\n<p>The first line <code>{ self, nixpkgs, ... }@ inputs:</code> defines the functions\nparameters: It’s important to understand that within the scope of the <code>outputs</code>\nfunction <code>nixpkgs</code> is available at the top-level because we explicitly passed it\nas an argument but for individual modules outside this flake the scope is lost,\nand you need to use <code>inputs.nixpkgs</code> (or equivalent)</p>\n<ol>\n<li>\n<p>It explicitly names the <code>self</code> attribute, making it directly accessible. The\nvariadic <code>...</code> ellipses part of the function signature is what allows all\nyour flake inputs to be brought into the function’s scope without having to\nlist each one explicitly.</p>\n</li>\n<li>\n<p>It destructures all other attributes (your defined <code>inputs</code>) into the\nfunctions scope.</p>\n</li>\n<li>\n<p>It gives you a convenient single variable, <code>inputs</code>, that refers to the\nentire attribute set passed to the <code>outputs</code> function. This allows you to\naccess inputs either individually (e.g. <code>nixpkgs</code>) or through the <code>inputs</code>\nvariable (e.g. <code>inputs.nixpkgs</code>).</p>\n</li>\n</ol>\n<p>You can also define additional arbitrary attributes, but these are the outputs\nthat Nix knows about.</p>\n<p>As you can see, the majority of the outputs within the outputs schema expect a\nderivation. This means that for packages, applications, formatters, checks, and\ndevelopment shells, you’ll be defining a Nix derivation—a set of instructions\nthat tells Nix how to build a particular software component. This is central to\nNix’s declarative nature.</p>\n<ul>\n<li>The command <code>nix flake show</code>, takes a flake URI and prints all the outputs of\nthe flake as a nice tree structure, mapping attribute paths to the types of\nvalues.</li>\n</ul>\n<pre><code class=\"language-bash\">  ~/players/third  3s\n❯ nix flake show\npath:/home/jr/players/third?lastModified=1748272555&amp;narHash=sha256-oNzkC6X9hA0MpOBmJSZ89w4znXxv4Q5EkFhp0ewehY0%3D\n├───nixosConfigurations\n│   └───testing: NixOS configuration\n└───nixosModules\n    └───default: NixOS module\n</code></pre>\n<p>To show you the structure of this little flake project:</p>\n<pre><code class=\"language-bash\">  ~/players\n❯ tree\n .\n├──  first\n│   ├──  flake.lock\n│   ├──  flake.nix\n│   └──  result -&gt; /nix/store/701vyaanmqchd2nnaq71y65v8ws11zx0-nixos-system-nixos-24.11.20250523.f09dede\n├──  second\n│   ├──  flake.lock\n│   └──  flake.nix\n└──  third\n    ├──  flake.lock\n    ├──  flake.nix\n    └──  result -&gt; /nix/store/mlszr5ws3xaly8m4q9jslgs31w6w76y2-nixos-system-nixos-24.11.20250523.f09dede\n</code></pre>\n<h2>Simple Example providing an output</h2>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  outputs = { self }: {\n    bada = \"bing\";\n  };\n}\n</code></pre>\n<p>You can then evaluate this specific output using <code>nix eval</code>:</p>\n<pre><code class=\"language-bash\">nix eval .#bada\n\"bing\"\n</code></pre>\n<h2>Outputs understood by Nix</h2>\n<p>While the attribute set that <code>outputs</code> returns may contain arbitrary attributes,\nmeaning any valid Nix value. Some of the standard outputs are understood by\nvarious <code>nix</code> utilities. <code>packages</code> is one of these:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs\";\n  };\n\n  outputs = { self, nixpkgs }: {\n    # this is the re-exporting part!\n    packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;\n  };\n}\n</code></pre>\n<ul>\n<li>Re-exporting happens when you take the value of <code>hello</code> in its standard\nderivation format, exactly as <code>nixpkgs</code> produces it and assign it to an\nattribute in your own flake’s outputs.\n<ul>\n<li>\n<p><code>packages.x86_64-linux.hello</code>(your flake’s output path) <code>=</code>\n<code> nixpkgs.legacyPackages.x86_64-linux.hello</code>(the source from the <code>nixpkgs</code>\nflake’s output)</p>\n</li>\n<li>\n<p>We’re saying, My flakes <code>hello</code> package is exactly the same as the <code>hello</code>\npackage found inside the <code>nixpkgs</code> input flake.</p>\n</li>\n<li>\n<p>It’s important to understand that within the scope of the <code>outputs</code> function\n(i.e. within your flake), <code>nixpkgs</code> is available at the top-level (i.e. the\n<code>= nixpkgs</code> part) because we explicitly passed it as an argument but for\nindividual modules outside of this flake the scope is lost, and\n<code>inputs.nixpkgs</code> is needed.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>The following command builds the reexported package:</p>\n<pre><code class=\"language-bash\">nix build .#hello\n</code></pre>\n<p>or run it with:</p>\n<pre><code class=\"language-bash\">nix run .#hello\n</code></pre>\n<p>You might notice <code>x86_64-linux</code> appearing in the package path, and there’s a\ngood reason for it. Flakes are designed to provide <em>hermetic evaluation</em>,\nmeaning their outputs should be identical regardless of the environment where\nthey’re built. A key factor in any build system is the platform (which combines\nthe architecture and operating system, like <code>x86_64-linux</code> or <code>aarch64-darwin</code>).</p>\n<p>Because of Nix’s commitment to reproducibility across different systems, any\nflake output that involves building software packages must explicitly specify\nthe platform. The standard approach is to structure these outputs as an\nattribute set where the names are platforms, and the values are the outputs\nspecific to that platform. For the packages output, each platform-specific value\nis itself an attribute set containing the various packages built for that\nparticular system.</p>\n<h2>Exporting Functions</h2>\n<p>This example outputs a <code>sayGoodbye</code> function, via the <code>lib</code> attribute, that\ntakes a name for its input and outputs a string saying Goodbye very nicely to\nthe person with that name:</p>\n<pre><code class=\"language-nix\">{\n  outputs = { self }: {\n    lib = {\n      sayGoodbye = name: \"Goodbye F*** Off, ${name}!\";\n    };\n  };\n}\n</code></pre>\n<p>You could then specify this flake as an input to another flake and use\n<code>sayGoodbye</code> however you’d like.</p>\n<p>Or load it into the <code>nix repl</code> like so:</p>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; :lf .\nnix-repl&gt; lib.sayGoodbye\n«lambda sayGoodbye @ /nix/store/665rwfvkwdx6kwvk9ldijp2a6jvcgv1n-source/flake.nix:4:20»\nnix-repl&gt; lib.sayGoodbye \"Jr\"\n\"Goodbye F*** Off, Jr!\"\n</code></pre>\n<ul>\n<li>As you can see, specifying <code>lib.sayGoodbye</code> without any arguments returns a\nfunction. (a lambda function)</li>\n</ul>\n<h2>Simplifying Multi-Platform Outputs with flake-utils</h2>\n<p>Manually repeating these platform definitions for every output (<code>packages</code>,\n<code>devShells</code>, <code>checks</code>, etc.) can quickly become verbose. This is where the\nflake-utils helper flake comes in handy. It provides utilities to reduce\nboilerplate when defining outputs for multiple systems.</p>\n<p>A commonly used function is <code>flake-utils.lib.eachDefaultSystem</code>, which\nautomatically generates outputs for common platforms (like <code>x86_64-linux</code>,\n<code>aarch64-linux</code>, <code>x86_64-darwin</code>, <code>aarch64-darwin</code>). This transforms your\noutputs definition from manually listing each system to a more concise\nstructure:</p>\n<h1>Example using flake-utils</h1>\n<pre><code class=\"language-nix\">{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\"; # Don't forget to add flake-utils to inputs!\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n    flake-utils,\n    ...\n  }:\n    flake-utils.lib.eachDefaultSystem (\n      system: let\n        pkgs = import nixpkgs {inherit system;};\n      in {\n        packages.hello = pkgs.hello; # Now directly defines 'hello' for the current 'system' # packages.default = self.packages.${system}.hello; # Optional default alias\n        devShells.default = pkgs.mkShell {\n          packages = [pkgs.hello];\n        };\n      }\n    );\n}\n</code></pre>\n<ul>\n<li>This flake-utils pattern is particularly useful for defining consistent\ndevelopment environments across platforms, which can then be activated simply\nby running <code>nix develop</code> in the flake’s directory.</li>\n</ul>\n<h3>Adding Formatter, Checks, and Devshell Outputs</h3>\n<p>This is a minimal flake for demonstration with a hardcoded <code>system</code>, for more\nportability:</p>\n<pre><code class=\"language-nix\">{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    home-manager.url = \"github:nix-community/home-manager\";\n    home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n    treefmt-nix.url = \"github:numtide/treefmt-nix\";\n   };\n\n  outputs = inputs@{ nixpkgs, home-manager, treefmt-nix, ... }: let\n\n    system = \"x86_64-linux\";\n    host = \"your-hostname-goes-here\";\n      # Define pkgs with allowUnfree\n    pkgs = import inputs.nixpkgs {\n      inherit system;\n      config.allowUnfree = true;\n    };\n\n        # Formatter configuration\n    treefmtEval = treefmt-nix.lib.evalModule pkgs ./lib/treefmt.nix;\n\nin {\n\n    formatter.${system} = treefmtEval.config.build.wrapper;\n\n    # Style check for CI\n    checks.${system}.style = treefmtEval.config.build.check self;\n\n    # Development shell\n    devShells.${system}.default = import ./lib/dev-shell.nix {\n      inherit inputs;\n    };\n\n\n    nixosConfigurations = {\n      hostname = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          home-manager.nixosModules.home-manager\n          {\n            home-manager.useGlobalPkgs = true;\n            home-manager.useUserPackages = true;\n            home-manager.users.jdoe = ./home.nix;\n\n            # Optionally, use home-manager.extraSpecialArgs to pass\n            # arguments to home.nix\n          }\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<p>And in <code>lib/treefmt.nix</code>:</p>\n<pre><code class=\"language-nix\"># treefmt.nix\n{\n  projectRootFile = \"flake.nix\";\n  programs = {\n    alejandra.enable = true;\n    deadnix.enable = true;\n    # rustfmt.enable = true;\n    # shellcheck.enable = true;\n    # prettier.enable = true;\n    statix.enable = true;\n    keep-sorted.enable = true;\n    # nixfmt = {\n    #   enable = true;\n    #   # strict = true;\n    # };\n  };\n  settings = {\n    global.excludes = [\n      \"LICENSE\"\n      \"README.md\"\n      \".adr-dir\"\n      \"nu_scripts\"\n      # unsupported extensions\n      \"*.{gif,png,svg,tape,mts,lock,mod,sum,toml,env,envrc,gitignore,sql,conf,pem,*.so.2,key,pub,py,narHash}\"\n      \"data-mesher/test/networks/*\"\n      \"nss-datamesher/test/dns.json\"\n      \"*.age\"\n      \"*.jpg\"\n      \"*.nu\"\n      \"*.png\"\n      \".jj/*\"\n      \"Cargo.lock\"\n      \"flake.lock\"\n      \"hive/moonrise/borg-key-backup\"\n      \"justfile\"\n    ];\n    formatter = {\n      deadnix = {\n        priority = 1;\n      };\n      statix = {\n        priority = 2;\n      };\n      alejandra = {\n        priority = 3;\n      };\n    };\n  };\n}\n</code></pre>\n<p>Now we have a few commands available to us in our flake directory:</p>\n<ul>\n<li>\n<p><code>nix fmt</code>: Will format your whole configuration consistently</p>\n</li>\n<li>\n<p><code>nix flake check</code>: While this command was already available, it is now tied to\ntreefmt’s check which will check the style of your syntax and provide\nsuggestions.</p>\n</li>\n</ul>\n<p>And this is <code>lib/dev-shell.nix</code>:</p>\n<pre><code class=\"language-nix\">{\n  inputs,\n  system ? \"x86_64-linux\",\n}: let\n  # Instantiate nixpkgs with the given system and allow unfree packages\n  pkgs = import inputs.nixpkgs {\n    inherit system;\n    config.allowUnfree = true;\n    overlays = [\n      # Add overlays if needed, e.g., inputs.neovim-nightly-overlay.overlays.default\n    ];\n  };\nin\n  pkgs.mkShell {\n    name = \"nixos-dev\";\n    packages = with pkgs; [\n      # Nix tools\n      nixfmt-rfc-style # Formatter\n      deadnix # Dead code detection\n      nixd # Nix language server\n      nil # Alternative Nix language server\n      nh # Nix helper\n      nix-diff # Compare Nix derivations\n      nix-tree # Visualize Nix dependencies\n\n      # Code editing\n      helix # Your editor\n\n      # General utilities\n      git\n      ripgrep\n      jq\n      tree\n    ];\n\n    shellHook = ''\n      echo \"Welcome to the NixOS development shell!\"\n      echo \"System: ${system}\"\n      echo \"Tools available: nixfmt, deadnix, nixd, nil, nh, nix-diff, nix-tree, helix, git, ripgrep, jq, tree\"\n    '';\n  }\n</code></pre>\n<p>Now you can run <code>nix develop</code> in the flake directory and if successfull, you’ll\nsee the <code>echo</code> commands above and you will have all the tools available in your\nenvironment without having to explicitly install them.</p>\n",
      "date_published": "2025-11-28T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/flake_examples_4.3.html",
      "url": "https://saylesss88.github.io/flakes/flake_examples_4.3.html",
      "title": "Flake outputs",
      "content_html": "<h1>Nix Flake Examples</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>This chapter provides practical examples to illustrate the concepts discussed in\n“Nix Flakes Explained.”</p>\n<h2>Example showing the extensibility of Flakes</h2>\n<p>NixOS modules and configurations offer us a powerful and composable way to\ndefine and share system configurations. Imagine we have several independent\n“players,” each with their own unique set of configurations or modules. How do\nwe combine these individual contributions into a single, cohesive system without\ndirectly altering each player’s original flake?</p>\n<p>This example demonstrates how flakes can extend and compose each other, allowing\nyou to layer configurations on top of existing ones. This is particularly useful\nwhen you want to:</p>\n<ul>\n<li>\n<p>Build upon a base configuration without modifying its source.</p>\n</li>\n<li>\n<p>Combine features from multiple independent flakes into a single system.</p>\n</li>\n<li>\n<p>Create specialized versions of an existing configuration.</p>\n</li>\n</ul>\n<p>Let’s simulate this by creating a players directory with three sub-directories:\n<code>first</code>, <code>second</code>, and <code>third</code>. Each of these will contain its own <code>flake.nix</code>.</p>\n<pre><code class=\"language-bash\">mkdir players\ncd players\nmkdir first\nmkdir second\nmkdir third\ncd first\n</code></pre>\n<p>Now create a <code>flake.nix</code> with the following contents:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-24.11\";\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n  }: {\n    nixosModules.default = {\n      config,\n      pkgs,\n      lib,\n      ...\n    }: {\n      # Create a file `/etc/first-file`\n      environment.etc.first-file.text = \"Hello player # 1!\";\n      boot.initrd.includeDefaultModules = false;\n      documentation.man.enable = false;\n      boot.loader.grub.enable = false;\n      fileSystems.\"/\".device = \"/dev/null\";\n      system.stateVersion = \"24.11\";\n    };\n    nixosConfigurations.testing = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      modules = [\n        self.nixosModules.default\n      ];\n    };\n  };\n}\n</code></pre>\n<ul>\n<li>This demonstrates using <code>self</code> to reference this flake from within its own\noutputs. This is the main use for <code>self</code> with flakes. Without <code>self</code>, I\nwouldn’t have a direct way to refer to the <code>nixosModules.default</code> that’s\ndefined within the same flake.</li>\n</ul>\n<p>Now in the <code>players/second</code> directory create this <code>flake.nix</code>:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-24.11\";\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n  }: {\n    nixosModules.default = {\n      config,\n      pkgs,\n      lib,\n      ...\n    }: {\n      # Create a file `/etc/second-file`\n      environment.etc.second-file.text = \"Hello player # 2!\";\n    };\n  };\n}\n</code></pre>\n<ul>\n<li><code>nixosModules.default</code> is a module which is a function that, when called by\nthe NixOS module system, returns an attribute set representing a piece of\nsystem configuration.\n<ul>\n<li>Within that attribute set, it specifies that the file <code>/etc/second-file</code>\nshould exist with “Hello player # 2!” as its content.</li>\n</ul>\n</li>\n</ul>\n<p>And finally in <code>players/third</code> create another <code>flake.nix</code>:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    first.url = \"/home/jr/players/first\";\n    nixpkgs.follows = \"first/nixpkgs\";\n    second = {\n      url = \"/home/jr/players/second\";\n      inputs.nixpkgs.follows = \"first/nixpkgs\";\n    };\n  };\n\n  outputs = {\n    self,\n    nixpkgs,\n    first,\n    second,\n  }:\n    first.outputs\n    // {\n      nixosConfigurations.testing = first.nixosConfigurations.testing.extendModules {\n        modules = [\n          second.nixosModules.default\n        ];\n      };\n    };\n}\n</code></pre>\n<ul>\n<li>You’ll have to change the locations to where you placed your <code>players</code>\ndirectory in the <code>inputs</code> above.</li>\n</ul>\n<p>In your <code>third</code> directory inspect it with:</p>\n<pre><code class=\"language-bash\">  ~/players/third\n❯ nix flake show\npath:/home/jr/players/third?lastModified=1748271697&amp;narHash=sha256-oNzkC6X9hA0MpOBmJSZ89w4znXxv4Q5EkFhp0ewehY0%3D\n├───nixosConfigurations\n│   └───testing: NixOS configuration\n└───nixosModules\n    └───default: NixOS module\n</code></pre>\n<p>and build it with:</p>\n<pre><code class=\"language-bash\">nix build .#nixosConfigurations.testing.config.system.build.toplevel\n</code></pre>\n<pre><code class=\"language-bash\">cat result/etc/first-file\nHello player # 1!\ncat result/etc/second-file\nHello player # 2!\n</code></pre>\n<p><strong>Understanding the Extension</strong></p>\n<p>As you saw in the <code>flake.nix</code> for the third player, we leveraged two key flake\nfeatures to combine and extend the previous configurations:</p>\n<ol>\n<li><strong>Attribute Set Union</strong> (<code>//</code> operator):</li>\n</ol>\n<pre><code class=\"language-nix\">outputs = { ..., first, second, ... }:\nfirst.outputs // { # ... your extensions here ...\n};\n</code></pre>\n<p>The <code>//</code> (attribute set union) operator allows us to take all the outputs from\n<code>first.outputs</code> (which includes its <code>nixosConfigurations</code> and <code>nixosModules</code>)\nand then overlay or add to them on the right-hand side. This means our third\nflake will inherit all the outputs from first, but we can then modify or add new\nones without changing the first flake itself.</p>\n<ol start=\"2\">\n<li><code>config.extendModules</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">    nixosConfigurations.testing = first.nixosConfigurations.testing.extendModules {\n      modules = [\n        second.nixosModules.default\n      ];\n    };\n</code></pre>\n<p>This is the core of the extension. We’re taking the testing NixOS configuration\ndefined in the first flake (<code>first.nixosConfigurations.testing</code>) and then\ncalling its <code>extendModules</code> function. This function allows us to inject\nadditional NixOS modules into an already defined system configuration. In this\ncase, we’re adding the default module from the second flake\n(<code>second.nixosModules.default</code>).</p>\n<p>By combining these techniques, the third flake successfully creates a NixOS\nconfiguration that includes both the settings from first (like <code>/etc/first-file</code>\nand the base system options) and the settings from second (like\n<code>/etc/second-file</code>), all without directly altering the first or second flakes.\nThis demonstrates the incredible power of flake extensibility for building\ncomplex, modular, and composable systems.</p>\n",
      "date_published": "2025-11-28T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Nix_Pull_Requests_11.html",
      "url": "https://saylesss88.github.io/Nix_Pull_Requests_11.html",
      "title": "Nix Pull Requests",
      "content_html": "<h1>Chapter 11</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/gruv16.png\" alt=\"gruv16\" /></p>\n<h2>Nix Pull Requests</h2>\n<p><strong>Pull requests</strong> communicate changes to a branch in a repository. Once a pull\nrequest is opened, you can review changes with collaborators and add follow-up\ncommits.</p>\n<ul>\n<li>\n<p>A <strong>pull request</strong> is a proposal to merge a set of changes from one branch\ninto another. In a pull request, collaborators can review and discuss the\nproposed set of changes before they integrate the changes into the main\ncodebase.</p>\n</li>\n<li>\n<p>Pull requests display the differences, or diffs, between the content in the\nsource branch and the content in the target branch.</p>\n</li>\n</ul>\n<pre><code class=\"language-mermaid\">graph LR\n    A[Your Local Repository] --&gt; B(Feature Branch);\n    B --&gt; C{GitHub Repository};\n    C -- \"Open Pull Request\" --&gt; D[Pull Request on GitHub];\n    D -- \"Review &amp; Discussion\" --&gt; D;\n    D -- \"Merge\" --&gt; E(Main Branch on GitHub);\n    E --&gt; F[Nixpkgs Users];\n</code></pre>\n<p><strong>Explanation of the Diagram</strong>:</p>\n<details>\n<summary> ✔️ Click to see Explanation </summary>\n<ul>\n<li>\n<p><strong>A[Your Local Repository]</strong>: This represents the copy of the Nixpkgs repo on\nyour computer where you make changes.</p>\n</li>\n<li>\n<p><strong>B (Feature Branch)</strong>: You create a dedicated branch (e.g.<code>my-pack-update</code>)\nto isolate your changes.</p>\n</li>\n<li>\n<p><strong>C {GitHub Repository}</strong>: This is the central online repo for Nixpkgs on\nGithub. You push your feature branch to this repo.</p>\n</li>\n<li>\n<p><strong>C – “Open Pull Request” – D [Pull Request on Github]</strong>: You initiate a\npull request from your feature branch to the main branch (usually <code>master</code> or\n<code>main</code>) through the GitHub interface.</p>\n</li>\n<li>\n<p><strong>D [Pull Request on GitHub]</strong>: This is where collaborators can see your\nproposed changes, discuss them, and provide feedback.</p>\n</li>\n<li>\n<p><strong>D – “Review &amp; Discussion” –&gt; D</strong>: The pull request facilitates\ncommunication and potential revisions based on the review.</p>\n</li>\n<li>\n<p><strong>D – “Merge” –&gt; E (Main Branch on GitHub)</strong>: Once the changes are approved,\nthey are merged into the main branch of the Nixpkgs repository.</p>\n</li>\n<li>\n<p><strong>E (Main Branch on GitHub)</strong>: The main branch now contains the integrated\nchanges.</p>\n</li>\n<li>\n<p><strong>E –&gt; F [Nixpkgs Users]</strong>): Eventually, these changes become available to\nall Nixpkgs users through updates to their Nix installations.</p>\n</li>\n</ul>\n</details>\n<p>Flakes often rely on having access to the full history of the Git repository to\ncorrectly determine dependencies, identify specific revisions of inputs, and\nevaluate the flake. Not in all situations will a shallow clone work and this is\none of them.</p>\n<p>If you have any changes to your local copy of Nixpkgs make sure to stash them\nbefore the following:</p>\n<pre><code class=\"language-bash\">git stash -u\n</code></pre>\n<ul>\n<li>This command saves your uncommited changes (including staged files)\ntemporarily. You can restore them later with <code>git stash pop</code></li>\n</ul>\n<p><strong>Step 1 Clone Nixpkgs Locally</strong></p>\n<p>If you don’t have Nixpkgs locally, you’ll need to clone it:</p>\n<pre><code class=\"language-bash\">git clone https://github.com/NixOS/nixpkgs.git\n</code></pre>\n<p><strong>Step 2 Find a Relevant Pull Request</strong></p>\n<p>To find specifig commits and releases:</p>\n<p><a href=\"https://status.nixos.org/\">status.nixos.org</a> provides the latest tested commits\nfor each release - use when pinning to specific commits. List of active release\nchannels - use when tracking latest channel versions.</p>\n<p>The complete list of channels is available at\n<a href=\"https://channels.nixos.org/\">nixos.org/channels</a></p>\n<p>To find a relevant PR you can go to:</p>\n<ul>\n<li>\n<p><a href=\"https://github.com/NixOS/nixpkgs/pulls\">Nixpkgs Pull Requests</a></p>\n</li>\n<li>\n<p>The following example actually uses the\n<a href=\"https://github.com/NixOS/nix/pulls\">Nix Pull Requests</a> the process is the\nsame, but that is an important distinction.</p>\n</li>\n<li>\n<p>In the Filters enter <code>stack trace</code> for this example.</p>\n</li>\n<li>\n<p>The pull request I chose was <a href=\"https://github.com/nixos/nix/pull/8623\">8623</a></p>\n</li>\n</ul>\n<p><strong>Step 3 Add the Remote Repository (if necessary)</strong></p>\n<p>If the pull request is from a different repository than your local clone (as in\nthe case of the <code>nix</code> PR while working in a <code>nixpkgs</code> clone), you need to add\nthat repository as a remote. It’s common to name the main Nixpkgs remote\n<code>origin</code> and other related repositories like <code>nix</code> as <code>upstream</code>.</p>\n<p>Assuming you are in your <code>nixpkgs</code> clone and want to test a PR from the <code>nix</code>\nrepository:</p>\n<pre><code class=\"language-bash\">git remote add upstream https://github.com/NixOS/nix.git\n</code></pre>\n<p><strong>Step 4 Fetch the Pull Request Changes</strong></p>\n<p>Fetch the Pull Request Information:</p>\n<pre><code class=\"language-bash\">git fetch upstream refs/pull/8623/head:pr-8623\n</code></pre>\n<ul>\n<li>This command fetches the branch named <code>head</code> from the pull request <code>8623</code> in\nthe <code>upstream</code> remote and creates a local branch named <code>pr-8623</code> that tracks\nit.</li>\n</ul>\n<p><strong>Output</strong>:</p>\n<details>\n<summary> ✔️ Output (Click to Enlarge) </summary>\n<pre><code>remote: Enumerating objects: 104651, done.\nremote: Counting objects: 100% (45/45), done.\nremote: Compressing objects: 100% (27/27), done.\nremote: Total 104651 (delta 33), reused 20 (delta 18), pack-reused 104606 (from 1)\nReceiving objects: 100% (104651/104651), 61.64 MiB | 12.56 MiB/s, done.\nResolving deltas: 100% (74755/74755), done.\nFrom https://github.com/NixOS/nix\n * [new ref]             refs/pull/8623/head -&gt; pr-8623\n * [new tag]             1.0                 -&gt; 1.0\n * [new tag]             1.1                 -&gt; 1.1\n * [new tag]             1.10                -&gt; 1.10\n * [new tag]             1.11                -&gt; 1.11\n * [new tag]             1.11.1              -&gt; 1.11.1\n * [new tag]             1.2                 -&gt; 1.2\n * [new tag]             1.3                 -&gt; 1.3\n * [new tag]             1.4                 -&gt; 1.4\n * [new tag]             1.5                 -&gt; 1.5\n * [new tag]             1.5.1               -&gt; 1.5.1\n * [new tag]             1.5.2               -&gt; 1.5.2\n * [new tag]             1.5.3               -&gt; 1.5.3\n * [new tag]             1.6                 -&gt; 1.6\n * [new tag]             1.6.1               -&gt; 1.6.1\n * [new tag]             1.7                 -&gt; 1.7\n * [new tag]             1.8                 -&gt; 1.8\n * [new tag]             1.9                 -&gt; 1.9\n * [new tag]             2.0                 -&gt; 2.0\n * [new tag]             2.2                 -&gt; 2.2\n</code></pre>\n</details>\n<p><strong>Step 5 Checkout the Local Branch:</strong></p>\n<pre><code class=\"language-bash\">git checkout pr-8623\n</code></pre>\n<p>Or with the <code>gh</code> cli:</p>\n<pre><code class=\"language-bash\">gh pr checkout 8623\n</code></pre>\n<h2>Build and Test the Changes</h2>\n<ul>\n<li>Now we want to see if the code changes introduced by the pull request actually\nbuild correctly within the Nix ecosystem.</li>\n</ul>\n<pre><code class=\"language-bash\">nix build\n</code></pre>\n<p><strong>Output:</strong></p>\n<details>\n<summary> ✔️ Output (Click to Enlarge) </summary>\n<pre><code class=\"language-bash\">error: builder for '/nix/store/rk86daqgf6a9v6pdx6vcc5b580lr9f09-nix-2.20.0pre20240115_20b4959.drv' failed with exit code 2;\n   last 25 log lines:\n   &gt;\n   &gt;         _NIX_TEST_ACCEPT=1 make tests/functional/lang.sh.test\n   &gt;\n   &gt;     to regenerate the files containing the expected output,\n   &gt;     and then view the git diff to decide whether a change is\n   &gt;     good/intentional or bad/unintentional.\n   &gt;     If the diff contains arbitrary or impure information,\n   &gt;     please improve the normalization that the test applies to the output.\n   &gt; make: *** [mk/lib.mk:90: tests/functional/lang.sh.test] Error 1\n   &gt; make: *** Waiting for unfinished jobs....\n   &gt; ran test tests/functional/selfref-gc.sh... [PASS]\n   &gt; ran test tests/functional/store-info.sh... [PASS]\n   &gt; ran test tests/functional/suggestions.sh... [PASS]\n   &gt; ran test tests/functional/path-from-hash-part.sh... [PASS]\n   &gt; ran test tests/functional/gc-auto.sh... [PASS]\n   &gt; ran test tests/functional/path-info.sh... [PASS]\n   &gt; ran test tests/functional/flakes/show.sh... [PASS]\n   &gt; ran test tests/functional/fetchClosure.sh... [PASS]\n   &gt; ran test tests/functional/completions.sh... [PASS]\n   &gt; ran test tests/functional/build.sh... [PASS]\n   &gt; ran test tests/functional/impure-derivations.sh... [PASS]\n   &gt; ran test tests/functional/build-delete.sh... [PASS]\n   &gt; ran test tests/functional/build-remote-trustless-should-fail-0.sh... [PASS]\n   &gt; ran test tests/functional/build-remote-trustless-should-pass-2.sh... [PASS]\n   &gt; ran test tests/functional/nix-profile.sh... [PASS]\n   For full logs, run:\n     nix log /nix/store/rk86daqgf6a9v6pdx6vcc5b580lr9f09-nix-2.20.0pre20240115_20b4959.drv\n</code></pre>\n</details>\n<ul>\n<li><strong><code>nix build</code></strong> (Part of the Nix Unified CLI):\n<ul>\n<li>\n<p>Declarative: when used within a Nix flake (<code>flake.nix</code>), <code>nix build</code> is a\nbit more declarative. It understands the outputs defined in your flake.</p>\n</li>\n<li>\n<p>Clearer Output Paths: <code>nix build</code> typically places build outputs in the\n<code>./result</code> directory by default (similar to <code>nix-build</code>’s <code>result</code> symlink)</p>\n</li>\n<li>\n<p>Better Error Reporting: It gives more informative error messages.</p>\n</li>\n<li>\n<p>Future Direction</p>\n</li>\n</ul>\n</li>\n</ul>\n<p><strong>Benefits of using <code>nix build</code>:</strong></p>\n<ul>\n<li>\n<p><strong>Flake Integration:</strong> <code>nix build</code> naturally understands the flake’s outputs.</p>\n</li>\n<li>\n<p><strong>Development Shells:</strong> When you are in a <code>nix develop</code> shell, <code>nix build</code> is\nthe more idiomatic way to build packages defined in your dev environment.</p>\n</li>\n<li>\n<p><strong>Consistency:</strong> Using the unified CLI promotes a more consistent workflow.</p>\n</li>\n</ul>\n<h2>Next Steps</h2>\n<p>As you can see this build failed, as for why the build failed, the key part of\nthe error message is:</p>\n<pre><code class=\"language-bash\">make: *** [mk/lib.mk:90: tests/functional/lang.sh.test] Error 1\n</code></pre>\n<ul>\n<li>This suggests that one of the functional tests (<code>lang.sh.test</code>) failed. This\nhappens when the expected output of the test doesn’t match the actual output.</li>\n</ul>\n<p>This can heppen when:</p>\n<ol>\n<li>\n<p>The test expectations are outdated due to changes in the codebase.</p>\n</li>\n<li>\n<p>The test captures environment-specific or transient outputs that are not\nproperly normalized.</p>\n</li>\n<li>\n<p>The test includes impure or non-deterministic information, making it hard to\nverify.</p>\n</li>\n</ol>\n<p>To address this, _NIX_TEST_ACCEPT=1 is used as an override mechanism that tells\nthe test framework: &gt; “Accept whatever output is generated as the new expected\nresult.”</p>\n<p>The message advises running:</p>\n<pre><code class=\"language-bash\">_NIX_TEST_ACCEPT=1 make tests/functional/lang.sh.test\n</code></pre>\n<ul>\n<li>This will regenerate the expected output files, allowing you to inspect what\nchanged with <code>git diff</code>:</li>\n</ul>\n<pre><code class=\"language-bash\">git diff tests/functional/lang.sh.test\n</code></pre>\n<ul>\n<li><strong>Verifies if Changes are Intentional:</strong> If the difference is reasonable and\nexpected (due to a legitimate update in the logic), you can commit these\nchanges to update the test suit. If not, you have to refine the test\nnormalization process further.</li>\n</ul>\n<p>If the changes seem valid, commit them:</p>\n<pre><code class=\"language-bash\">git add tests/functional/lang.sh.test\ngit commit -m \"Update expected test output for lang.sh.test\"\n</code></pre>\n<p>Running the following will provide the full logs:</p>\n<pre><code class=\"language-bash\">nix log /nix/store/rk86daqgf6a9v6pdx6vcc5b580lr9f09-nix-2.20.0pre20240115_20b4959.drv\n</code></pre>\n<h3>Conclusion</h3>\n<p>Testing Nixpkgs pull requests is a vital part of contributing to a healthy and\nreliable Nix ecosystem. By following these steps, you can help ensure that\nchanges are well-vetted before being merged, ultimately benefiting all Nix\nusers. Your efforts in testing contribute significantly to the quality and\nstability of Nixpkgs.</p>\n",
      "date_published": "2025-11-27T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/unenc/unenc_impermanence.html",
      "url": "https://saylesss88.github.io/installation/unenc/unenc_impermanence.html",
      "title": "Unencrypted BTRFS Impermanence with Flakes",
      "content_html": "<h1>Unencrypted BTRFS Impermanence with Flakes</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>Figure 1: Impermanence Logo: Image of the Impermanence logo. Sourced from the</p>\n<p><a href=\"https://github.com/nix-community/impermanence\">Impermanence repo</a></p>\n<p>This guide is for an unencrypted setup, there are a few links at the end for\nencrypted setups. This guide follows the previous\n<a href=\"https://saylesss88.github.io/installation/unencrypted_setups.html\">minimal install guide</a>\nbut you should be able to adjust it carefully to meet your needs.</p>\n<p>This section details how to set up impermanence on your NixOS system using BTRFS\nsubvolumes. With impermanence, your operating system’s root filesystem will\nreset to a pristine state on each reboot, while designated directories and files\nremain persistent. This provides a highly reliable and rollback-friendly system.</p>\n<p>In NixOS, “state” is any data or condition of the system that isn’t defined in\nyour declarative configuration. The impermanence approach aims to make this\nstate temporary (ephemeral) or easily resettable, so your system always matches\nyour configuration and can recover from unwanted changes or corruption.</p>\n<h2>Impermanence: The Concept and Its BTRFS Implementation</h2>\n<p>In a traditional Linux system, most of this state is stored on the disk and\npersists indefinitely unless manually deleted or modified. However, this can\nlead to configuration drift, where the system accumulates changes (e.g., log\nfiles, temporary files, or unintended configuration tweaks) that make it harder\nto reproduce or maintain.</p>\n<p>Impermanence, in the context of operating systems, refers to a setup where the\nmajority of the system’s root filesystem (<code>/</code>) is reset to a pristine state on\nevery reboot. This means any changes made to the system (e.g., installing new\npackages, modifying system files outside of configuration management, creating\ntemporary files) are discarded upon shutdown or reboot.</p>\n<h2>What Does Impermanence Do?</h2>\n<p>Impermanence is a NixOS approach that makes the system stateless (or nearly\nstateless) by wiping the root filesystem (<code>/</code>) on each boot, ensuring a clean,\npredictable starting point. Only explicitly designated data (persistent state)\nis preserved across reboots, typically stored in specific locations like the\n/nix/persist subvolume. This is possible because NixOS can boot with only the\n<code>/boot</code>, and <code>/nix</code> directories. This achieves:</p>\n<ol>\n<li>Clean Root Filesystem:</li>\n</ol>\n<ul>\n<li>\n<p>The root subvolume is deleted and recreated on each boot, erasing transient\nstate (e.g., temporary files, runtime data).</p>\n</li>\n<li>\n<p>This ensures the system starts fresh, reducing clutter and making it behave\ncloser to a declarative system defined by your NixOS configuration.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li>Selective Persistence:</li>\n</ol>\n<ul>\n<li>\n<p>Critical state (e.g., user files, logs, system configuration) is preserved in\ndesignated persistent subvolumes (e.g., /nix/persist, /var/log, /var/lib) or\nfiles.</p>\n</li>\n<li>\n<p>You control exactly what state persists by configuring\n<code>environment.persistence.\"/nix/persist\"</code> or other mechanisms.</p>\n</li>\n<li>\n<p>❗ The understanding around persisting <code>/var/lib/nixos</code> seems to be evolving.\nSee,The importance of persisting <code>/var/lib/nixos</code> See also necessary system\nstate</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Reproducibility and Security:</li>\n</ol>\n<ul>\n<li>\n<p>By wiping transient state, impermanence prevents unintended changes from\naccumulating, making the system more reproducible.</p>\n</li>\n<li>\n<p>It enhances security by ensuring sensitive temporary data (e.g., /tmp, runtime\ncredentials) is erased on reboot.</p>\n</li>\n</ul>\n<h3>Getting Started</h3>\n<ol>\n<li>Add impermanence to your flake.nix. You will change the hostname in the flake\nto match your networking.hostName.</li>\n</ol>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    disko.url = \"github:nix-community/disko/latest\";\n    disko.inputs.nixpkgs.follows = \"nixpkgs\";\n    impermanence.url = \"github:nix-community/impermanence\";\n  };\n\n  outputs = inputs@{ nixpkgs, ... }: {\n    nixosConfigurations = {\n      hostname = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          inputs.disko.nixosModules.disko\n          inputs.impermanence.nixosModules.impermanence\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<ol start=\"2\">\n<li>Discover where your root subvolume is located with <code>findmnt</code>:</li>\n</ol>\n<p>Before configuring impermanence, it’s crucial to know the device path and\nsubvolume path of your main BTRFS partition where the root filesystem (/) is\nlocated. This information is needed for the mount command within the\nimpermanence script.</p>\n<pre><code class=\"language-bash\">findmnt /\nTARGET   SOURCE         FSTYPE OPTIONS\n/        /dev/disk/by-partlabel/disk-main-root[/root]\n                        btrfs  rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=275,sub\n</code></pre>\n<p>From the SOURCE column, note the full path, including the device (e.g.,\n<code>/dev/disk/by-partlabel/disk-main-root</code>) and the subvolume in brackets (e.g.,\n<code>[/root]</code>). You will use the device path in the next step</p>\n<p><code>/dev/disk/by-partlabel/disk-main-root</code> is a symlink to the actual device path\n(e.g. <code>/dev/nvme0n1p2</code>), but using the partlabel is generally more robust for\nscripts.</p>\n<ol start=\"3\">\n<li>Create an impermanence.nix:</li>\n</ol>\n<p>Now, create a new file named <code>impermanence.nix</code> in your configuration directory\n(i.e. your flake directory). This file will contain all the specific settings\nfor your impermanent setup, including BTRFS subvolume management and persistent\ndata locations. Since this file is right next to your <code>configuration.nix</code>,\nyou’ll just add an <code>imports = [ ./impermanence.nix</code> ] to your\n<code>configuration.nix</code> apply it to your configuration.</p>\n<pre><code class=\"language-nix\">{lib, ...}: {\n  #  Reset root subvolume on boot\n  boot.initrd.postResumeCommands = lib.mkAfter ''\n    mkdir /btrfs_tmp\n      mount /dev/disk/by-partlabel/disk-main-root /btrfs_tmp # CONFIRM THIS IS CORRECT FROM findmnt\n      if [[ -e /btrfs_tmp/root ]]; then\n        mkdir -p /btrfs_tmp/old_roots\n        timestamp=$(date --date=\"@$(stat -c %Y /btrfs_tmp/root)\" \"+%Y-%m-%-d_%H:%M:%S\")\n        mv /btrfs_tmp/root \"/btrfs_tmp/old_roots/$timestamp\"\n      fi\n\n      delete_subvolume_recursively() {\n        IFS=$'\\n'\n        for i in $(btrfs subvolume list -o \"$1\" | cut -f 9- -d ' '); do\n          delete_subvolume_recursively \"/btrfs_tmp/$i\"\n        done\n        btrfs subvolume delete \"$1\"\n      }\n\n      for i in $(find /btrfs_tmp/old_roots/ -maxdepth 1 -mtime +30); do\n        delete_subvolume_recursively \"$i\"\n      done\n\n      btrfs subvolume create /btrfs_tmp/root\n      umount /btrfs_tmp\n  '';\n\n  # Use /persist as the persistence root, matching Disko's mountpoint\n  environment.persistence.\"/nix/persist\" = {\n    hideMounts = true;\n    directories = [\n      \"/etc\" # System configuration (Keep this here for persistence via bind-mount)\n      \"/var/spool\" # Mail queues, cron jobs\n      \"/srv\" # Web server data, etc.\n      \"/root\"\n    ];\n    files = [\n    ];\n  };\n}\n</code></pre>\n<p>With btrfs subvolumes since each directory is its own subvolume, when the root\nis wiped on reboot the subvolumes are untouched.</p>\n<h3>Applying Your Impermanence Configuration</h3>\n<p>Once you have completed all the steps and created or modified the necessary\nfiles (<code>flake.nix</code>, <code>impermanence.nix</code>), you need to apply these changes to your\nNixOS system.</p>\n<ol>\n<li>Navigate to your NixOS configuration directory (where your flake.nix is\nlocated).</li>\n</ol>\n<pre><code class=\"language-bash\">cd /path/to/your/flake\n</code></pre>\n<ol start=\"2\">\n<li>Rebuild and Switch: Execute the <code>nixos-rebuild switch</code> command. This command\nwill:</li>\n</ol>\n<ul>\n<li>\n<p>Evaluate your flake.nix and the modules it imports (including your new\nimpermanence.nix).</p>\n</li>\n<li>\n<p>Build a new NixOS system closure based on your updated configuration.</p>\n</li>\n<li>\n<p>Activate the new system configuration, making it the current running system.</p>\n</li>\n</ul>\n<blockquote>\n<p>❗ NOTE: On the first rebuild after setting up impermanence, you may find that\nyou’re not in the password database or cannot log in/sudo. This occurs because\nthe initial state of your new ephemeral root filesystem, including /etc (where\nuser passwords are stored), is fresh. It has to do with the timing of when\nenvironment.persistence takes effect during the first boot.</p>\n</blockquote>\n<blockquote>\n<p>To avoid this password issue, before your first nixos-rebuild switch for\nimpermanence, run:</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /nix/persist/etc # Ensure the target directory exists\nsudo cp -a /etc/* /nix/persist/etc\n</code></pre>\n<ul>\n<li>This copies your current /etc directory contents (including existing user\npasswords) into your persistent &gt;&gt;storage.</li>\n<li>Crucially: You must also ensure that <code>/etc</code> is explicitly included in your\n<code>environment.persistence.\"/nix/persist\"</code>.directories list in your\n<code>impermanence.nix</code> like we did above, (or main configuration). This\nconfigures &gt;NixOS to persistently bind-mount <code>/nix/persist/etc</code> over <code>/etc</code>\non every subsequent boot. Once these steps are done and you reboot, your\nuser passwords should function correctly, and future rebuilds will &gt; not\npresent this problem.</li>\n</ul>\n</blockquote>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --flake .#hostname # Replace 'hostname' with your actual system hostname\n</code></pre>\n<ol start=\"3\">\n<li>Perform an Impermanence Test (Before Reboot):</li>\n</ol>\n<ul>\n<li>Before you reboot, create a temporary directory and file in a non-persistent\nlocation. Since you haven’t explicitly added <code>/imperm_test</code> to your\n<code>environment.persistence.\"/nix/persist\"</code> directories, this file should not\nsurvive a reboot.</li>\n</ul>\n<pre><code class=\"language-bash\">mkdir /imperm_test\necho \"This should be Gone after Reboot\" | sudo tee /imperm_test/testfile\nls -l /imperm_test/testfile # Verify the file exists\ncat /imperm_test/testfile # Verify content\n</code></pre>\n<ol start=\"4\">\n<li>Reboot Your System: For the impermanence setup to take full effect and for\nyour root filesystem to be reset for the first time, you must reboot your\nmachine.</li>\n</ol>\n<pre><code class=\"language-bash\">sudo reboot\n</code></pre>\n<ol start=\"5\">\n<li>Verify Impermanence (After Reboot):</li>\n</ol>\n<ul>\n<li>After the system has rebooted, check if the test directory and file still\nexist:</li>\n</ul>\n<pre><code class=\"language-bash\">ls -l /imperm_test/testfile\n</code></pre>\n<p>You should see an output like <code>ls: cannot access '/imperm_test/testfile'</code>: No\nsuch file or directory. This confirms that the <code>/imperm_test</code> directory and its\ncontents were indeed ephemeral and were removed during the reboot process,\nindicating your impermanence setup is working correctly!</p>\n<p>Your system should now come up with a fresh root filesystem, and only the data\nspecified in your <code>environment.persistence.\"/nix/persist\"</code> configuration will be\npersistent.</p>\n<h3>Recovery with nixos-enter and chroot</h3>\n<p>This is if you followed the minimal_install guide, it will need to be changed\nfor a different disk layout.</p>\n<p><a href=\"https://en.wikipedia.org/wiki/Chroot\">Chroot</a> is an operation that changes the\napparent root directory for the current running process and their children. A\nprogram that is run in such a modified environment cannot access files and\ncommands outside that environmental directory tree. This modified environment is\ncalled a chroot jail. –NixOS wiki</p>\n<p><code>nixos-enter</code> allows you to access a NixOS installation from a NixOS rescue\nsystem. To use, setup <code>/mnt</code> as described in the\n<a href=\"https://nixos.org/manual/nixos/stable/#sec-installation\">installation manual</a></p>\n<p>🛠️ Recovery: Chroot into Your NixOS Btrfs+Impermanence System</p>\n<p>Take note of your layout from commands like:</p>\n<pre><code class=\"language-bash\">sudo fdisk -l\nlsblk\nsudo btrfs subvol list /\n</code></pre>\n<p>Also inspect your <code>disk-config.nix</code> to ensure you refer to the correct <code>subvol=</code>\nnames.</p>\n<p>If you need to repair your system (e.g., forgot root password, fix a broken\nconfig, etc.), follow these steps to chroot into your NixOS install:</p>\n<ol>\n<li>Boot a Live ISO</li>\n</ol>\n<p>Boot from a NixOS (or any recent Linux) live USB.</p>\n<p>Open a terminal and become root:</p>\n<pre><code class=\"language-bash\">sudo -i\n</code></pre>\n<ol start=\"2\">\n<li>Identify Your Devices</li>\n</ol>\n<p>Your main disk is <code>/dev/nvme0n1</code></p>\n<ul>\n<li>\n<p>EFI partition: <code>/dev/nvme0n1p1</code> (mounted at <code>/boot</code>)</p>\n</li>\n<li>\n<p>Root partition: <code>/dev/nvme0n1p2</code> (Btrfs, with subvolumes)</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Mount the Btrfs Root Subvolume</li>\n</ol>\n<p>First, mount the Btrfs partition somewhere temporary (not as / yet):</p>\n<pre><code class=\"language-bash\">mount -o subvol=root,compress=zstd,noatime /dev/nvme0n1p2 /mnt\n</code></pre>\n<ol start=\"4\">\n<li>Mount Other Subvolumes</li>\n</ol>\n<p>Now mount your other subvolumes as defined in your <code>disko.nix</code>:</p>\n<pre><code class=\"language-bash\"># Mount Other Subvolumes\n# (Ensure /mnt directories are created for each *mountpoint*)\n\n# Home\nmkdir -p /mnt/home\nmount -o subvol=home,compress=zstd,noatime /dev/nvme0n1p2 /mnt/home\n\n# IMPORTANT: No separate mount for /mnt/home/user, as it's a nested subvolume\n# and handled by the /home mount.\n\n# Nix store\nmkdir -p /mnt/nix\nmount -o subvol=nix,compress=zstd,noatime /dev/nvme0n1p2 /mnt/nix\n\n# Nix persist\nmkdir -p /mnt/nix/persist\n# CRITICAL: Based our disko.nix, the subvolume name is 'persist', not 'nix/persist'\nmount -o subvol=persist,compress=zstd,noatime /dev/nvme0n1p2 /mnt/nix/persist\n\n# /var/log\nmkdir -p /mnt/var/log\nmount -o subvol=log,compress=zstd,noatime /dev/nvme0n1p2 /mnt/var/log\n\n# /var/lib\nmkdir -p /mnt/var/lib\n# Confirmed: The subvolume named 'lib' is mounted to /var/lib\nmount -o subvol=lib,compress=zstd,noatime /dev/nvme0n1p2 /mnt/var/lib\n</code></pre>\n<p>Note: If you get “subvolume not found,” check the subvolume names with\n<code>btrfs subvol list /mnt</code>.</p>\n<ol start=\"5\">\n<li>Mount the EFI Partition</li>\n</ol>\n<pre><code class=\"language-bash\">mkdir -p /mnt/boot mount /dev/nvme0n1p1 /mnt/boot\n</code></pre>\n<ol start=\"6\">\n<li>(Optional) Mount Virtual Filesystems</li>\n</ol>\n<pre><code class=\"language-bash\">mount --bind /dev /mnt/dev mount --bind /proc /mnt/proc mount --bind /sys\n/mnt/sys mount --bind /run /mnt/run\n</code></pre>\n<ol start=\"7\">\n<li>Chroot</li>\n</ol>\n<pre><code class=\"language-bash\">chroot /mnt /run/current-system/sw/bin/bash\n</code></pre>\n<p>or, if using a non-NixOS live system:</p>\n<pre><code class=\"language-bash\">nixos-enter\n</code></pre>\n<p>(You may need to install nixos-enter with nix-shell -p nixos-enter.) 8. You’re\nIn!</p>\n<p>You can now run nixos-rebuild, reset passwords, or fix configs as needed. 🔎</p>\n<p>📓 Notes</p>\n<ul>\n<li>\n<p>Adjust <code>compress=zstd,noatime</code> if your config uses different mount options.</p>\n</li>\n<li>\n<p>For impermanence, make sure to mount all persistent subvolumes you need.</p>\n</li>\n<li>\n<p>If you use swap, you may want to enable it too (e.g., swapon /dev/zram0 if\nrelevant).</p>\n</li>\n</ul>\n<p>You can now recover, repair, or maintain your NixOS system as needed!</p>\n<h4>Related Material</h4>\n<ul>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Change_root\">Change root (chroot</a></p>\n</li>\n<li>\n<p><a href=\"https://www.mankier.com/8/nixos-enter\">nixos-enter</a></p>\n</li>\n<li>\n<p><a href=\"https://grahamc.com/blog/erase-your-darlings/\">erase your darlings</a></p>\n</li>\n<li>\n<p><a href=\"https://haseebmajid.dev/posts/2024-07-30-how-i-setup-btrfs-and-luks-on-nixos-using-disko/\">Guide for Btrfs with LUKS</a></p>\n</li>\n<li>\n<p><a href=\"https://notashelf.dev/posts/impermanence\">notashelf impermanence</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Impermanence\">NixOS wiki Impermanence</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nix-community/impermanence\">nix-community impermanence module</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-11-24T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/nix_language.html",
      "url": "https://saylesss88.github.io/nix/nix_language.html",
      "title": "Nix Lang",
      "content_html": "<h1>Nix Language</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![lambda1](../images/lambda1.png) -->\n<h2>Nix Expression Language Syntax Overview</h2>\n<p>The Nix language is designed for conveniently creating and composing\n<em>derivations</em> precise descriptions of how contents of files are used to derive\nnew files. –<a href=\"https://nix.dev/manual/nix/2.28/language/\">Nix Reference Manual</a></p>\n<p>Nix is often described as “JSON with functions.” It’s a declarative language\nwhere you define outcomes, not step-by-step instructions. Instead of writing\nsequential code, you create expressions that describe data structures,\nfunctions, and dependencies. These expressions are evaluated lazily, meaning Nix\ncomputes values only when needed, making it efficient for managing large\nsystems.</p>\n<p>You can plug most of the following into the <code>nix repl</code> I’m showing it in a\nsingle code block here for brevity:</p>\n<pre><code class=\"language-nix,editable\"># Comments Look Like This!\n\n# Strings\n\"This is a string\"          # String literal\n\n''\none\ntwo                        # multi-line String\nthree\n''\n\n(\"foo\" + \"bar\")           # =&gt; \"foobar\"\n\n\"foo\" != \"bar\"   # Inequality test  # =&gt; true\n\n!false      # =&gt; true\n\n(\"Home dir is ${builtins.getEnv \"HOME\"}\")  # String Interpolation\n# =&gt; \"Home dir is /home/jr\"\n\n\"3 6 ${builtins.toString 9}\"\n# =&gt; \"3 6 9\"\n\n\"goodbye ${ { d = \"world\";}.d}\"\n# =&gt; \"goodbye world\"\n\n# Booleans\n\n(false &amp;&amp; true)    # AND         # =&gt; false\n\n(true || false)    # OR         # =&gt; true\n\n(if 6 &lt; 9 then \"yay\" else \"nay\")  # =&gt; \"yay\"\n\nnull      # Null Value\n\n679       # Integer\n\n(6 + 7 + 9) # =&gt; 22   # Addition\n\n(9 - 3  - 2) # =&gt; 4   # Subtraction\n\n(6 / 3)  # =&gt; 2       # Division\n6.79      # Floating Point\n\n/etc/nixos      # Absolute Path\n\n../modules/nixos/boot.nix    # relative\n\n# Let expressions\n\n(let a = \"2\"; in                   # Let expressions are a way to create variables\na + a + builtins.toString \"4\")\n# =&gt; \"224\"\n\n(let first = \"firstname\"; in\n\"lastname \" first)\n# =&gt; \"lastname firstname\"\n\n# Lists\n\n[ 1 2 \"three\" \"bar\" \"baz\" ]   # lists are whitespace separated\n\nbuiltins.elemAt [ 1 2 3 4 5 ] 3\n# =&gt; 4\n\nbuiltins.length [ 1 2 3 4 ]\n# =&gt; 4\n\n# Attrsets\n\n{ first = \"Jim\"; last = \"Bo\"; }.last # Attribute selection\n# =&gt; \"Bo\"\n\n{ a = 1; b = 3; } // { c = 4; b = 2; }   # Attribute Set merging\n# =&gt; { a = 1; b = 2; c = 4; }               # Right Side takes precedence\n\nbuiltins.listToAttrs [ { name = \"Jr\"; value = \"Jr Juniorville\"; } {name = \"$\"; value = \"JR\"; } { name = \"jr\"; value = \"jr\nville\"; }]\n# =&gt; { \"$\" = \"JR\"; Jr = \"Jr Juniorville\"; jr = \"jrville\"; }\n\n# Control Flow\n\nif 2 * 2 == 4\nthen \"yes!\"\nelse \"no!\"\n# =&gt; \"yes!\"\n\nassert 2 * 2\n== 4; \"yes!\"\n# =&gt; \"yes!\"\n\nwith builtins;\nhead [ 5 6 7 ]\n# =&gt; 5\n\n# or\n\nbuiltins.head[ 5 6 7 ]\n\ninherit pkgs     # pkgs = pkgs;\nsrc;             # src = src;\n</code></pre>\n<h3>Understanding Laziness</h3>\n<p>Nix expressions are evaluated lazily, meaning Nix computes values only when\nneeded. This is a powerful feature that makes Nix efficient for managing large\nsystems, as it avoids unnecessary computations.</p>\n<p>For example, observe how <code>a</code> is never evaluated in the following <code>nix-repl</code>\nsession:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; let a = builtins.div 4 0; b = 6; in b\n6\n</code></pre>\n<ul>\n<li>Since <code>a</code> isn’t used in the final result, there’s no division by zero error.</li>\n</ul>\n<h3>Strings and String Interpolation</h3>\n<p><strong>Strings</strong>: Strings are enclosed in double quotes (<code>\"</code>) or two single quotes\n(<code>''</code>).</p>\n<pre><code class=\"language-nix\">nix-repl&gt; \"stringDaddy\"\n\"stringDaddy\"\nnix-repl&gt; ''\n  This is a\n  multi-line\n  string\n''\n\"This is a\\nmulti-line\\nstring.\\n\"\n</code></pre>\n<p><a href=\"https://nix.dev/manual/nix/2.24/language/string-interpolation\">string interpolation</a>.\nis a language feature where a string, path, or attribute name can contain an\nexpressions enclosed in <code>${ }</code>. This construct is called an <em>interpolated\nstring</em>, and the expression inside is an <em>interpolated expression</em>.</p>\n<p>Rather than writing:</p>\n<pre><code class=\"language-nix\">let path = \"/usr/local\"; in \"--prefix=${path}\"\n</code></pre>\n<p>This evaluates to <code>\"--prefix=/usr/local\"</code>. Interpolated expressions must\nevaluate to a string, path, or an attribute set with an <code>outPath</code> or\n<code>__toString</code> attribute.</p>\n<h3>Attribute Sets</h3>\n<p><strong>Attribute sets</strong> are all over Nix code and deserve their own section, they are\nname-value pairs wrapped in curly braces, where the names must be unique:</p>\n<pre><code class=\"language-nix\">{\n  string = \"hello\";\n  int = 8;\n}\n</code></pre>\n<p>Attribute names usually don’t need quotes.</p>\n<p>You can access attributes using <em>dot notation</em>:</p>\n<pre><code class=\"language-nix\">let person = { name = \"Alice\"; age = 30; }; in person.name\n\"Alice\"\n</code></pre>\n<p>You will sometimes see attribute sets with <code>rec</code> prepended. This allows access\nto attributes within the set:</p>\n<pre><code class=\"language-nix\">rec {\n  x = y;\n  y = 123;\n}.x\n</code></pre>\n<p><strong>Output</strong>: <code>123</code></p>\n<p>or</p>\n<pre><code class=\"language-nix\">rec {\n  one = 1;\n  two = one + 1;\n  three = two + 1;\n}\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-nix\"> {\n  one = 1;\n  three = 3;\n  two = 2;\n }\n</code></pre>\n<pre><code class=\"language-nix\"># This would fail:\n{\n  one = 1;\n  two = one + 1;  # Error: undefined variable 'one'\n  three = two + 1;\n}\n</code></pre>\n<p>Recursive sets introduce the danger of <em>infinite recursion</em> For example:</p>\n<pre><code class=\"language-nix\">rec {\n  x = y;\n  y = x;\n}.x\n</code></pre>\n<p>Will crash with an <code>infinite recursion encountered</code> error message.</p>\n<p>The\n<a href=\"https://nix.dev/manual/nix/2.24/language/operators.html#update\">attribute set update operator</a>\nmerges two attribute sets.</p>\n<p><strong>Example</strong>:</p>\n<pre><code class=\"language-nix\">{ a = 1; b = 2; } // { b = 3; c = 4; }\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-nix\">{ a = 1; b = 3; c = 4; }\n</code></pre>\n<p>However, names on the right take precedence, and updates are shallow.</p>\n<p><strong>Example</strong>:</p>\n<pre><code class=\"language-nix\">{ a = { b = 1; }; } // { a = { c = 3; }; }\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-nix\">{ a = { c = 3; }; }\n</code></pre>\n<p>Above, key <code>b</code> was completely removed, because the whole <code>a</code> value was replaced.</p>\n<p><strong>Inheriting Attributes</strong></p>\n<ul>\n<li>Click to see Output:</li>\n</ul>\n<pre><code class=\"language-nix\">let x = 123; in\n{\n  inherit x;\n  y = 456;\n}\n</code></pre>\n<p>is equivalent to</p>\n<pre><code class=\"language-nix\">let x = 123; in\n{\n  x = x;\n  y = 456;\n}\n</code></pre>\n<p>which are both equivalent to</p>\n<pre><code class=\"language-nix\">{\n  x = 123;\n  y = 456;\n}\n</code></pre>\n<blockquote>\n<p>❗: This works because <code>x</code> is added to the lexical scope by the <code>let</code>\nconstruct.</p>\n</blockquote>\n<p>Now that we understand attribute sets lets move on to functions, a powerful\nfeature of the Nix language that gives you the ability to reuse and share\nlogical pieces of code.</p>\n<h3>Functions(lambdas):</h3>\n<p>Functions in Nix help you build reusable components and are the the building\nblocks of Nix. In the next chapter we’ll go even further into Nix functions and\nhow to use them but I will touch on them here.</p>\n<p>Nix functions have this form:</p>\n<pre><code class=\"language-nix\">pattern: body\n</code></pre>\n<p>The following is a function that expects an integer and returns it increased by\n1:</p>\n<pre><code class=\"language-nix\">x: x + 1   # lambda function, not bound to a variable\n</code></pre>\n<p>The pattern tells us what the argument of the function has to look like, and\nbinds variables in the body to (parts of) the argument.</p>\n<pre><code class=\"language-nix\">(x: x + 5) 200\n205\n</code></pre>\n<p>They are all lambdas (i.e. anonymous functions without names) until we assign\nthem to a variable like the following example.</p>\n<p>Functions are defined using this syntax, where <code>x</code> and <code>y</code> are attributes passed\ninto the function:</p>\n<pre><code class=\"language-nix\">{\n  my_function = x: y: x + y;\n}\n</code></pre>\n<p>The code below calls a function called <code>my_function</code> with the parameters <code>2</code> and\n<code>3</code>, and assigns its output to the <code>my_value</code> field:</p>\n<pre><code class=\"language-nix\">{\n  my_value = my_function 2 3;\n}\nmy_value\n5\n</code></pre>\n<p>The body of the function automatically returns the result of the function.\nFunctions are called by spaces between it and its parameters. No commas are\nneeded to separate parameters.</p>\n<p>The following is a function that expects an attribute set with required\nattributes <code>a</code> and <code>b</code> and concatenates them:</p>\n<pre><code class=\"language-nix\">{ a, b }: a + b\n</code></pre>\n<p><strong>Default Values in Functions</strong>:</p>\n<p>Functions in Nix can define <strong>default values</strong> for their arguments. This allows\nfor more flexible function calls where some arguments are optional.</p>\n<pre><code class=\"language-nix\">{ x, y ? \"foo\", z ? \"bar\" }: z + y + x\n</code></pre>\n<ul>\n<li>Specifies a function that only requires an attribute named <code>x</code>, but optionally\naccepts <code>y</code> and <code>z</code>.</li>\n</ul>\n<p><strong>@-patterns in functions</strong>:</p>\n<p>An <code>@-pattern</code> provides a means of referring to the whole value being matched by\nthe function’s argument pattern, in addition to destructuring it. This is\nespecially useful when you want to access attributes that are not explicitly\ndestructured in the pattern:</p>\n<pre><code class=\"language-nix\">args@{ x, y, z, ... }: z + y + x + args.a\n# or\n{ x, y, z, ... } @ args: z + y + x + args.a\n</code></pre>\n<ul>\n<li>\n<p>Here, <code>args</code> is bound to the argument as <em>passed</em>, which is further matched\nagainst the pattern <code>{ x, y, z, ... }</code>. The <code>@-pattern</code> makes mainly sense\nwith an ellipsis(<code>...</code>) as you can access attribute names as <code>a</code>, using\n<code>args.a</code>, which was given as an additional attribute to the function.</p>\n</li>\n<li>\n<p>We will expand on Functions in\n<a href=\"https://saylesss88.github.io/Understanding_Nix_Functions_2.html\">This Chapter</a></p>\n</li>\n</ul>\n<h3>If, Let, and With Expressions</h3>\n<p>Nix is a pure expression language, meaning every construct evaluates to a value\n— there are no statements. Because of this, <strong>if expressions</strong> in Nix work\ndifferently than in imperative languages, where conditional logic often relies\non statements (<code>if</code>, <code>elsif</code>, etc.).</p>\n<p><strong>If expressions in Nix</strong>:</p>\n<p>Since everything in Nix is an expression, an <code>if</code> expression must always produce\na value:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; a = 6\nnix-repl&gt; b = 10\nnix-repl&gt; if a &gt; b then \"yes\" else \"no\"\n\"no\"\n</code></pre>\n<p>Here, <code>\"no\"</code> is the result because <code>a</code>(6) is not greater than <code>b</code>(10). Notice\nthat there’s no separate conditional statement – the entire construct evaluates\nto a value.</p>\n<p>Another example, integrating built-in functions:</p>\n<pre><code class=\"language-nix\">{\n  key = if builtins.pathExists ./path then \"YES\" else \"NO!\";\n}\n</code></pre>\n<p>If <code>./path</code> exists it will evaluate to the value <code>\"YES\"</code> or else it will\nevaluate to <code>\"NO!\"</code>.</p>\n<p>Thus, the final result of the expression would be:</p>\n<pre><code class=\"language-nix\">{ key = \"YES\"; }\n# or\n{ key = \"NO!\"; }\n</code></pre>\n<p>Since Nix does not have statements, Nix’s <code>if</code> statements behave more like\n<a href=\"https://en.wikipedia.org/wiki/Ternary_conditional_operator\">ternary operators</a>\n(<code>condition ? value_if_true : value_if_false</code>) in other languages.</p>\n<p><strong>Let expressions</strong>:</p>\n<p>Let expressions in Nix is primarily a mechanism for local variable binding and\nscoping. It allows you to define named values that are only accessible within\nthe <code>in</code> block of the <code>let</code> expression. This is useful for keeping code clean\nand avoiding repitition.</p>\n<p>For example:</p>\n<pre><code class=\"language-nix\">let\n  a = \"foo\";\n  b = \"fighter\";\nin a + b\n\"foofighter\"\n</code></pre>\n<p>Here, <code>a</code> and <code>b</code> are defined inside the <code>let</code> block, and their values are used\nin the <code>in</code> expression. Since everything in Nix is an expression, <code>a + b</code>\nevaluates to <code>\"foofighter\"</code></p>\n<p><strong>Using Let Expressions Inside Attribute Sets</strong></p>\n<p>Let expressions are commonly used when defining attribute sets (Click for\noutput):</p>\n<pre><code class=\"language-nix\">let\n  appName = \"nix-app\";\n  version = \"1.0\";\nin {\n  name = appName;\n  fullName = appName + \"-\" + version;\n}\n~{\n~  name = \"nix-app\";\n~  fullName = \"nix-app-1.0\";\n~}\n</code></pre>\n<p>This allows you to reuse values within an attribute set, making the code more\nmodular and preventing duplication.</p>\n<p><strong>Let Expressions in Function Arguments</strong></p>\n<p>You can also use let expressions within function arguments to define\nintermediate values before returning an output:</p>\n<pre><code class=\"language-nix\">{ pkgs, lib }:\nlet\n  someVar = \"hello\";\n  otherVar = \"world\";\nin\n{ inherit pkgs lib someVar otherVar; }\n</code></pre>\n<p>Result:</p>\n<pre><code class=\"language-nix\">{\n  pkgs = &lt;value&gt;;\n  lib = &lt;value&gt;;\n  someVar = \"hello\";\n  otherVar = \"world\";\n}\n</code></pre>\n<p>Here, <code>inherit</code> brings <code>pkgs</code> and <code>lib</code> into the resulting attribute set,\nalongside the locally defined variables <code>someVar</code> and <code>otherVar</code>.</p>\n<p><strong>Key Takeaways</strong>:</p>\n<ul>\n<li>\n<p>Let expressions allow local variable bindings that are only visible inside the\nin block. They also help avoid repitition and improve readability.</p>\n</li>\n<li>\n<p>Commonly used inside attribute sets or function arguments.</p>\n</li>\n<li>\n<p>Their scope is limited to the expression in which they are declared.</p>\n</li>\n</ul>\n<p><strong>With expressions</strong>:</p>\n<p>A <code>with</code> expression in Nix is primarily used to simplify access to attributes\nwithin an attribute set. Instead of repeatedly referring to a long attribute\npath, with temporarily brings the attributes into scope, allowing direct access\nwithout prefixing them.</p>\n<p><strong>Basic Example: Reducing Attribute Path Usage</strong></p>\n<p>Consider the following expressions:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; longName = { a = 3; b = 4; }\nnix-repl&gt; longName.a + longName.b\n7\n</code></pre>\n<p>Here, we must explicitly reference <code>longName.a</code> and <code>longName.b</code>. Using a <code>with</code>\nexpression simplifies this:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; with longName; a + b\n7\n</code></pre>\n<p>Now, within the scope of the with expression, <code>a</code> and <code>b</code> are accessible without\nprefixing them with <code>longName</code>.</p>\n<p><strong>Practical Use Case: Working with <code>pkgs</code></strong></p>\n<p>One of the most common uses of <code>with</code> that you’ll see is when dealing with\npackages from <code>nixpkgs</code> is writing the following:</p>\n<pre><code class=\"language-nix\">{ pkgs }:\nwith pkgs; {\n  myPackages = [ vim git neofetch ];\n}\n</code></pre>\n<p>Instead of writing this:</p>\n<pre><code class=\"language-nix\">{ pkgs }:\n{\n  myPackages = [ pkgs.vim pkgs.git pkgs.neofetch ];\n}\n</code></pre>\n<blockquote>\n<p>Tip: Overusing <code>with lib;</code> or <code>with pkgs;</code> can reduce clarity, it may be fine\nfor smaller modules where the scope is limited. For larger configurations,\nexplicit references (<code>pkgs.something</code>) often make dependencies clearer and\nprevent ambiguity.</p>\n</blockquote>\n<h3>Nix Language Quirks</h3>\n<ol>\n<li><code>with</code> gets less priority than <code>let</code>. This can be confusing, especially if\nyou like to write <code>with pkgs;</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">nix-repl&gt; pkgs = { x = 2; }\n\nnix-repl&gt; with pkgs; x\n2\n\nnix-repl&gt; with pkgs; let x = 4; in x\n4\n</code></pre>\n<p>This shows us that the <code>let</code> binding overrides the <code>with</code> binding.</p>\n<pre><code class=\"language-nix\">let x = 4; in with pkgs; x\n4\n</code></pre>\n<p>Still returns <code>4</code>, but the reasoning is different. The <code>with</code> expression doesn’t\ndefine new bindings; it simply makes attributes from <code>pkgs</code> available as\nunqualified names. However, because <code>let x = 4</code> is <strong>outside</strong> the <code>with</code>, it\nalready extablished <code>x = 4</code>, so when <code>with pkgs; x</code> is evaluated inside, <code>x</code>\nstill refers to the <strong>outer</strong> <code>let</code> binding, not the one from <code>pkgs</code>.</p>\n<ol start=\"2\">\n<li>Default values aren’t bound in <code>@-patterns</code></li>\n</ol>\n<p>In the following example, calling a function that binds a default value <code>\"baz\"</code>\nto the attribute <code>b</code> of an argument using an alias (<code>@</code>) pattern, with an empty\nattribute set as argument, results in the alias variable inputs being bound to\nthe original empty attribute set instead of including the default value:</p>\n<pre><code class=\"language-nix\">(inputs@(b ? \"baz\"): inputs) {}\n</code></pre>\n<p>Output:</p>\n<pre><code class=\"language-nix\">{}\n</code></pre>\n<p>This happens because the alias <code>inputs@</code> binds to the argument as passed, before\nthe default value for <code>b</code> is applied.</p>\n<p>The syntax requires curly brackets around the attribute set pattern for\ncorrectness, so the fixed syntax would be:</p>\n<pre><code class=\"language-nix\">(inputs@{b ? \"baz\"}: inputs) {}\n</code></pre>\n<p>However, even with this fix, the inputs alias still refers to the original\nargument without defaults applied. So the quirk persists, showing how default\nvalues in <code>@-patterns</code> do not propagate into the aliased variable.</p>\n<ol start=\"3\">\n<li>Destructuring function arguments:</li>\n</ol>\n<pre><code class=\"language-nix\">nix-repl&gt; f = { x ? 2, y ? 4 }: x + y\n\nnix-repl&gt; f { }\n6\n</code></pre>\n<p>The function <code>f</code> takes an attribute set with default values (<code>x = 2</code>, <code>y = 4</code>)</p>\n<p>When called with <code>{}</code> (an empty set), it falls back to the default values\n(<code>2 + 4</code> -&gt; <code>6</code>)</p>\n<p>Using <code>@args</code> to capture the entire input set:</p>\n<p>The <code>@args</code> syntax allows us to retain access to the full attribute set, even\nafter destructuring:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; f = { x ? 1, y ? 2, ... }@args: with args; x + y + z\n\nnix-repl&gt; f { z = 3; }\n6\n</code></pre>\n<p>The <code>{ x ? 1, y ? 2, ... }</code> syntax means <code>x</code> and <code>y</code> have defaults, while <code>...</code>\nallows additional attributes.</p>\n<p><code>@args</code> binds the entire attribute set (<code>args</code>) so that we can access <code>z</code>, which\nwouldn’t be destructured by default.</p>\n<p>When calling <code>f { z = 3; }</code>, we pass an extra attribute (<code>z = 3</code>), making\n<code>x + y + z</code> → <code>1 + 2 + 3 = 6</code>.</p>\n<ol start=\"4\">\n<li>Imports and namespaces</li>\n</ol>\n<p>There is a keyword import, but it’s equivalent in other languages is eval. It\ncan be used for namespacing too:</p>\n<pre><code class=\"language-nix\">let\n  pkgs = import &lt;nixpkgs&gt; {};\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  pkgs.runCommand (lib.strings.removePrefix \"....\n</code></pre>\n<p>consider using <code>import</code> here as using <code>qualified import ...</code> in Haskell or\n<code>import ...</code> in Python.</p>\n<p>Another way of importing is with <code>import ...;</code>, which corresponds to Python\n<code>from ... import *</code>.</p>\n<p>But because of not very great IDE support in Nix, <code>with import ...;</code> is\ndiscouraged. Rather use inherit, especially if you are targeting source code for\nNix newcomers:</p>\n<pre><code class=\"language-nix\">let\n  lib = import &lt;nixpkgs/lib&gt;;\n  inherit (lib.strings)\n    removePrefix removeSuffix\n  ;\n  inherit (lib.lists)\n    isList init drop\n  ;\nin\n  removePrefix ...\n</code></pre>\n<p><code>inherit</code> has higher priority than <code>with</code>, and conflicts with <code>let</code></p>\n<pre><code class=\"language-nix\">nix-repl&gt; let pkgs = { x = 1; }; x = 2; x = 3; inherit (pkgs) x; in x\nerror: attribute ‘x’ at (string):1:31 already defined at (string):1:24\n</code></pre>\n<p>This makes it a sane citizen of Nix lanugage… except it has a twin, called\n<code>{ inherit ...; }</code>. They DON’T do the same - <code>let inherit ...</code> adds\nlet-bindings, and <code>{ inherit ...; }</code> adds attributes to a record.\n–<a href=\"https://nixos.wiki/wiki/Nix_Language_Quirks\">https://nixos.wiki/wiki/Nix_Language_Quirks</a></p>\n<ol start=\"5\">\n<li>Only attribute names can be interpolated, not Nix code:</li>\n</ol>\n<pre><code class=\"language-nix\">nix-repl&gt; let ${\"y\"} = 4; in y\n4\n\nnix-repl&gt; with { ${\"y\"} = 4; }; y\n4\n\nlet y = 1; x = ${y}; in x\nerror: syntax error, unexpected DOLLAR_CURLY\n</code></pre>\n<p><strong>Conclusion</strong></p>\n<ul>\n<li>\n<p><code>let</code> bindings introduce new local values and override anything from <code>with</code>.</p>\n</li>\n<li>\n<p><code>with</code> doesn’t create bindings - it only makes attributes available within its\nscope.</p>\n</li>\n<li>\n<p>The order matters: If <code>let x = 4</code> is outside <code>with</code>, then <code>x = 4</code> already\nexists before <code>with</code> runs, so <code>with pkgs; x</code> resolves to <code>4</code>, not the value\nfrom <code>pkgs</code>.</p>\n</li>\n</ul>\n<h4>Resources</h4>\n<details>\n<summary> ✔️ Resources (Click to Expand) </summary>\n<p>A few resources to help get you started with the Nix Language, I have actually\ngrown to love the language. I find it fairly simple but powerful!</p>\n<ul>\n<li>\n<p><a href=\"https://nix.dev/tutorials/nix-language.html\">nix.dev nixlang-basics</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.24/language/\">Nix Language Overview</a></p>\n</li>\n<li>\n<p><a href=\"https://learnxinyminutes.com/nix/\">learn nix in y minutes</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/tazjin/nix-1p\">nix onepager</a></p>\n</li>\n<li>\n<p><a href=\"https://zero-to-nix.com/concepts/nix-language/\">zero-to-nix nix lang</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/04-basics-of-language.html\">nix-pills basics of nixlang</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/04-basics-of-language\">Basics of the Language Pill</a></p>\n</li>\n</ul>\n</details>\n<pre><code class=\"language-nix\">builtins.head[ 5 6 7 ]\n</code></pre>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/nix_package_manager.html",
      "url": "https://saylesss88.github.io/nix/nix_package_manager.html",
      "title": "Nix Package Manager",
      "content_html": "<h1>Nix Package Manager</h1>\n<details>\n<summary> Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![nix99](../images/nix99.png) -->\n<h2>Nix Package Manager</h2>\n<p>Nix is a <em>purely functional package manager</em>. This means that it treats packages\nlike values in purely functional programming languages – they are built by\nfunctions that don’t have side-effects, and they never change after they have\nbeen built.</p>\n<p>Nix stores packages in the <em>Nix store</em>, usually the directory <code>/nix/store</code>,\nwhere each package has its own unique subdirectory such as:</p>\n<pre><code class=\"language-bash\">/nix/store/y53c0lamag5wpx7vsiv7wmnjdgq97yd6-yazi-25.5.14pre20250526_74a8ea9\n</code></pre>\n<p>You can use the Nix on most Linux distributions and Mac OS also has good support\nfor Nix. It should work on most platforms that support POSIX threads and have a\nC++11 compiler.</p>\n<p>When I install Nix on a distro like Arch Linux I usually use the Zero to Nix\ninstaller as it automates several steps, such as enabling flakes by default:</p>\n<pre><code class=\"language-bash\">curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install\n</code></pre>\n<p>If you have concerns about the “curl to Bash” approach you could examine the\ninstallation script\n<a href=\"https://raw.githubusercontent.com/DeterminateSystems/nix-installer/main/nix-installer.sh\">here</a>\nthen download and run it:</p>\n<pre><code class=\"language-bash\">curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix &gt; nix-installer.sh\nchmod +x nix-installer.sh\n./nix-installer.sh install\n</code></pre>\n<p>I got the above commands from\n<a href=\"https://zero-to-nix.com/start/install/\">zero-to-nix</a></p>\n<p>The main difference between using the nix package manager on another\ndistribution and NixOS is that NixOS uses Nix not just for package management\nbut also to manage the system configuration (e.g., to build config files in\n<code>/etc</code>).</p>\n<p><a href=\"https://nix-community.github.io/home-manager/\">Home Manager</a> is a Nix-powered\ntool for reproducible management of the contents of the users’ home directories.\nThis includes programs, configuration files, environment variables, and\narbitrary files. Home manager uses the same module system as NixOS.</p>\n<p>Now that we’ve discussed some of the basics of the Nix package manager, lets see\nhow it is used to build and manage software in NixOS.</p>\n<h2>Channels</h2>\n<p>Nix packages are distributed through Nix channels; mechanisms for distributing\nNix expressions and the associated binary caches for them. Channels are what\ndetermine which versions your packages have. (i.e. <em>stable</em> or <em>unstable</em>). A\nchannel is a name for the latest “verified” git commits in Nixpkgs. Each channel\nrepresents a different policy for what “verified” means. Whenever a new commit\nin <code>Nixpkgs</code> passes the verification process, the respective channel is updated\nto point to that new commit.</p>\n<p>While channels provide a convenient way to get the latest stable or unstable\npackages, they introduce a challenge for strict reproducibility. Because a\nchannel like <code>nixos-unstable</code> is constantly updated, fetching packages from it\ntoday might give you a different set of package versions than fetching from it\ntomorrow, even if your configuration remains unchanged. This “rolling release”\nnature at a global level can make it harder to share and reproduce exact\ndevelopment environments or system configurations across different machines or\nat different points in time.</p>\n<h2>Channels vs. Flakes Enhancing Reproducibility</h2>\n<p>Before the introduction of <strong>Nix Flakes</strong>, channels were the primary mechanism\nfor sourcing <code>Nixpkgs</code>. While functional, they posed a challenge for exact\nreproducibility because they point to a moving target (the latest commit on a\nbranch). This meant that a <code>nix-build</code> command run yesterday might produce a\ndifferent result than one run today, simply because the channel updated.</p>\n<p>Nix Flakes were introduced to address this. Flakes bring a built-in,\nstandardized way to define the exact inputs to a Nix build, including the\nprecise Git revision of <code>Nixpkgs</code> or any other dependency.</p>\n<p>Here’s a quick comparison:</p>\n<table><thead><tr><th style=\"text-align: left\">Feature</th><th style=\"text-align: left\">Nix Channels (traditional)</th><th style=\"text-align: left\">Nix Flakes (modern approach)</th></tr></thead><tbody>\n<tr><td style=\"text-align: left\"><strong>Input Source</strong></td><td style=\"text-align: left\">Global system configuration (<code>nix-channel --update</code>)</td><td style=\"text-align: left\">Explicitly defined in <code>flake.nix</code> (e.g., <code>github:NixOS/nixpkgs/nixos-23.11</code>)</td></tr>\n<tr><td style=\"text-align: left\"><strong>Reproducibility</strong></td><td style=\"text-align: left\">“Rolling release”; less reproducible across time/machines</td><td style=\"text-align: left\">Highly reproducible due to locked inputs (<code>flake.lock</code>)</td></tr>\n<tr><td style=\"text-align: left\"><strong>Dependency Mgmt.</strong></td><td style=\"text-align: left\">Implicitly managed by global channel</td><td style=\"text-align: left\">Explicitly declared and version-locked within <code>flake.nix</code></td></tr>\n<tr><td style=\"text-align: left\"><strong>Sharing</strong></td><td style=\"text-align: left\">Relies on users having same channel version</td><td style=\"text-align: left\">Self-contained; <code>flake.lock</code> ensures everyone gets same inputs</td></tr>\n<tr><td style=\"text-align: left\"><strong>Learning Curve</strong></td><td style=\"text-align: left\">Simpler initial setup, but tricky reproducibility debugging</td><td style=\"text-align: left\">Higher initial learning curve, but simplifies reproducibility</td></tr>\n</tbody></table>\n<p>The ability of Flakes to “lock” the exact version of all dependencies in a\n<code>flake.lock</code> file is a game-changer for collaboration and long-term\nreproducibility, ensuring that your Nix configuration builds the same way, every\ntime, everywhere.</p>\n<h2>Nixpkgs</h2>\n<p><strong>Nixpkgs</strong> is the largest repository of Nix packages and NixOS modules.</p>\n<p>For <strong>NixOS</strong> users, <code>nixos-unstable</code> channel branch is the rolling release,\nwhere the packages are tested and must pass integration tests.</p>\n<p>For <strong>standalone Nix</strong> users, <code>nixpkgs-unstable</code> channel branch is the rolling\nrelease, where packages pass only basic build tests and are upgraded often.</p>\n<p>For Flakes, as mentioned above they don’t use channels so <code>nixpkgs</code> will be\nlisted as an <code>input</code> to your flake. (e.g.,\n<code>inputs.nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";</code>) When using flakes\nyou can actually disable channels and actually recommended to avoid conflicts\nbetween traditional channel-based workflows and the flake system.</p>\n<h3>Updates</h3>\n<p>The mechanism for updating your Nix environment differs fundamentally between\nchannels and flakes, directly impacting reproducibility and control.</p>\n<h4>Updating with Channels (Traditional Approach)</h4>\n<p>With channels, updates are a global operation that pulls the latest state of a\nspecific branch.</p>\n<p><strong>How it works</strong>: You typically use <code>nix-channel --update</code> to fetch the latest\ncommit from the channels you’ve subscribed to. For instance,\n<code>sudo nix-channel --update nixos</code> (for NixOS) or <code>nix-channel --update nixpkgs</code>\n(for <code>nix-env</code> on other Linux distributions).</p>\n<p><strong>Implication</strong>: This command updates your local system’s understanding of what\n“nixos” or “nixpkgs-unstable” means. From that point on, any\n<code>nixos-rebuild switch</code>, <code>nix-env -iA</code>, or <code>nix-build</code> commands that implicitly\nor explicitly refer to <code>nixpkgs</code> will use this newly updated version.</p>\n<p><strong>Reproducibility Challenge</strong>: The update itself is not recorded in your\nconfiguration files. If you share your <code>configuration.nix</code> with someone, they\nmight run <code>nix-channel --update</code> on a different day and get a different set of\npackage versions because the channel has moved. This makes it challenging to\nguarantee that two users building the “same” configuration will get identical\nresults. You’re effectively relying on the implicit, globally managed state of\nyour channels.</p>\n<h4>Updating with Flakes (Modern Approach)</h4>\n<p><strong>Flakes</strong>, by contrast, use a more explicit and localized update mechanism tied\nto your <code>flake.lock</code> file.</p>\n<p><strong>How it works</strong>: When you define a <code>flake.nix</code>, you specify the exact URL\n(e.g., a Git repository with a specific branch or tag) for each input. When you\nfirst use a flake, Nix resolves these URLs to a precise Git commit hash and\nrecords this hash, along with a content hash, in a <code>flake.lock</code> file.</p>\n<p>To update your flake inputs, you run <code>nix flake update</code>.</p>\n<p><strong>Implication</strong>: This command goes to each input’s specified URL (e.g.,\n<code>github:NixOS/nixpkgs/nixos-unstable</code>) and fetches the latest commit for that\ninput. It then updates your <code>flake.lock</code> file with the new, precise Git commit\nhash and content hash for that input. Your <code>flake.nix</code> itself doesn’t change,\nbut the <code>flake.lock</code> file now points to newer versions of your dependencies.</p>\n<p><strong>Reproducibility Advantage</strong>: The <code>flake.lock</code> file acts as a manifest of your\nexact dependency versions.</p>\n<p><strong>Sharing</strong>: When you share your flake (the <code>flake.nix</code> and <code>flake.lock</code> files),\nanyone using it will fetch precisely the same Git commit hashes recorded in the\n<code>flake.lock</code>, guaranteeing identical inputs and thus, identical builds (assuming\nthe same system architecture).</p>\n<p><strong>Updating Selectively</strong>: You can update individual inputs within your flake by\nspecifying them: <code>nix flake update nixpkgs</code>. This provides fine-grained control\nover which parts of your dependency graph you want to advance.</p>\n<p><strong>Rolling Back</strong>: Because the <code>flake.lock</code> explicitly records the versions, you\ncan easily revert to a previous state by checking out an older <code>flake.lock</code> from\nyour version control system.</p>\n<p><strong>In essence</strong>: Channels involve a global “pull” of the latest branch state,\nmaking reproducibility harder to guarantee across time and machines. Flakes,\nhowever, explicitly pin all inputs in <code>flake.lock</code>, and updates involve\nexplicitly refreshing these pins, providing strong reproducibility and version\ncontrol out of the box.</p>\n<h3>Managing software with Nix</h3>\n<p><strong>Derivation Overview</strong></p>\n<p>In Nix, the process of managing software starts with <strong>package definitions</strong>.\nThese are files written in the Nix language that describe how a particular piece\nof software should be built. These package definitions, when processed by Nix,\nare translated into derivations.</p>\n<p>At its core, a derivation in Nix is a blueprint or a recipe that describes how\nto build a specific software package or any other kind of file or directory.\nIt’s a declarative specification of:</p>\n<ul>\n<li>\n<p><strong>Inputs</strong>: What existing files or other derivations are needed as\ndependencies.</p>\n</li>\n<li>\n<p><strong>Build Steps</strong>: The commands that need to be executed to produce the desired\noutput.</p>\n</li>\n<li>\n<p><strong>Environment</strong>: The specific environment (e.g., build tools, environment\nvariables) required for the build process.</p>\n</li>\n<li>\n<p><strong>Outputs</strong>: The resulting files or directories that the derivation produces.</p>\n</li>\n</ul>\n<p>Think of a package definition as the initial instructions, and the derivation as\nthe detailed, low-level plan that Nix uses to actually perform the build.</p>\n<p>Again, a derivation is like a blueprint that describes how to build a specific\nsoftware package or any other kind of file or directory.</p>\n<p><strong>Key Characteristics of Derivations:</strong></p>\n<ul>\n<li>\n<p><strong>Declarative</strong>: You describe the desired outcome and the inputs, not the\nexact sequence of imperative steps. Nix figures out the necessary steps based\non the builder and args.</p>\n</li>\n<li>\n<p><strong>Reproducible</strong>: Given the same inputs and build instructions, a derivation\nwill always produce the same output. This is a cornerstone of Nix’s\nreproducibility.</p>\n</li>\n<li>\n<p><strong>Tracked by Nix</strong>: Nix keeps track of all derivations and their outputs in\nthe Nix store. This allows for efficient management of dependencies and\nensures that different packages don’t interfere with each other.</p>\n</li>\n<li>\n<p><strong>Content-Addressed</strong>: The output of a derivation is stored in the Nix store\nunder a unique path that is derived from the hash of all its inputs and build\ninstructions. This means that if anything changes in the derivation, the\noutput will have a different path.</p>\n</li>\n</ul>\n<p>Here’s a simple Nix derivation that creates a file named hello in the Nix store\ncontaining the text “Hello, World!”:</p>\n<details>\n<summary> ✔️ Hello World Derivation Example (Click to expand):</summary>\n<pre><code class=\"language-nix\">{pkgs ? import &lt;nixpkgs&gt; {}}:\npkgs.stdenv.mkDerivation {\n  name = \"hello-world\";\n\n  dontUnpack = true;\n\n  # No need for src = null; when dontUnpack = true;\n  # src = null;\n\n  buildPhase = ''\n     # Create a shell script that prints \"Hello, World!\"\n    echo '#!${pkgs.bash}/bin/bash' &gt; hello-output-file # Shebang line\n    echo 'echo \"Hello, World!\"' &gt;&gt; hello-output-file # The command to execute\n    chmod +x hello-output-file # Make it executable\n  '';\n\n  installPhase = ''\n    mkdir -p $out/bin\n    cp hello-output-file $out/bin/hello # Copy the file from build directory to $out/bin\n  '';\n\n  meta = {\n    description = \"A simple Hello World program built with Nix\";\n    homepage = null;\n    license = pkgs.lib.licenses.unfree; # licenses.mit is often used as well\n    maintainers = [];\n  };\n}\n</code></pre>\n<p>And a <code>default.nix</code> with the following contents:</p>\n<pre><code class=\"language-nix\">{ pkgs ? import &lt;nixpkgs&gt; {} }:\n\nimport ./hello.nix { pkgs = pkgs; }\n</code></pre>\n<ul>\n<li>\n<p><code>{ pkgs ? import &lt;nixpkgs&gt; {} }</code>: This is a function that takes an optional\nargument <code>pkgs</code>. We need Nixpkgs to access standard build environments like\n<code>stdenv</code>.</p>\n</li>\n<li>\n<p><code>pkgs.stdenv.mkDerivation { ... }:</code> This calls the mkDerivation function from\nthe standard environment (stdenv). mkDerivation is the most common way to\ndefine software packages in Nix.</p>\n</li>\n<li>\n<p><code>name = \"hello-world\";</code>: Human-readable name of the derivation</p>\n</li>\n<li>\n<p>The rest are the build phases and package metadata.</p>\n</li>\n</ul>\n<p>To use the above derivation, save it as a <code>.nix</code> file (e.g. <code>hello.nix</code>). Then\nbuild the derivation using,:</p>\n<pre><code class=\"language-bash\">nix-build\nthis derivation will be built:\n  /nix/store/9mc855ijjdy3r6rdvrbs90cg2gf2q160-hello-world.drv\nbuilding '/nix/store/9mc855ijjdy3r6rdvrbs90cg2gf2q160-hello-world.drv'...\nRunning phase: patchPhase\nRunning phase: updateAutotoolsGnuConfigScriptsPhase\nRunning phase: configurePhase\nno configure script, doing nothing\nRunning phase: buildPhase\nRunning phase: installPhase\nRunning phase: fixupPhase\nshrinking RPATHs of ELF executables and libraries in /nix/store/2ydxh5pd9a6djv7npaqi9rm6gmz2f73b-hello-world\nchecking for references to /build/ in /nix/store/2ydxh5pd9a6djv7npaqi9rm6gmz2f73b-hello-world...\npatching script interpreter paths in /nix/store/2ydxh5pd9a6djv7npaqi9rm6gmz2f73b-hello-world\nstripping (with command strip and flags -S -p) in  /nix/store/2ydxh5pd9a6djv7npaqi9rm6gmz2f73b-hello-world/bin\n/nix/store/2ydxh5pd9a6djv7npaqi9rm6gmz2f73b-hello-world\n</code></pre>\n<ul>\n<li>\n<p>Nix will execute the <code>buildPhase</code> and <code>installPhase</code></p>\n</li>\n<li>\n<p>After a successful build, the output will be in the Nix store. You can find\nthe exact path by looking at the output of the nix build command (it will be\nsomething like <code>/nix/store/your-hash-hello-world</code>).</p>\n</li>\n</ul>\n<p>Run the “installed” program:</p>\n<pre><code class=\"language-bash\">./result/bin/hello\n</code></pre>\n<ul>\n<li>This will execute the <code>hello</code> file from the Nix store and print\n<code>\"Hello, World!\"</code>.</li>\n</ul>\n</details>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/nixLang/nix_paths.html",
      "url": "https://saylesss88.github.io/nix/nixLang/nix_paths.html",
      "title": "Nix Paths",
      "content_html": "<h1>Nix Paths</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>The following examples are done with a local <code>nixpkgs</code> clone located at\n<code>~/src/nixpkgs</code></p>\n<p>Paths in Nix always need a <code>/</code> in them and always expand to absolute paths\nrelative to your current directory.</p>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; ./.\n/home/jr/src/nixpkgs\nnix-repl&gt; ./. + \"/lib\"\n/home/jr/src/nixpkgs/lib\n</code></pre>\n<p>Nix does <em>path normalization</em> every time you append strings, so if you just add\na slash <code>/</code> its not actually there:</p>\n<pre><code class=\"language-bash\">nix-repl&gt; ./.\n/home/jr/src/nixpkgs\nnix-repl&gt; ./. + \"/\"\n/home/jr/src/nixpkgs\nnix-repl&gt; ./. + \"/\" + \"lib\"\n/home/jr/src/nixpkgslib\nnix-repl&gt; \"${./.}/lib\"\n# using ${./.} causes a store copy\ncopying '/homr/jr/src/nixpkgs' to the store\n\"/nix/store/3z9fzx8z03wslxvri5syv3jnnhn5fkbd-nixpkgs/lib\"\nnix-repl&gt; \"${toString ./.}/lib\"\n# using toString avoids making a store copy\n\"/home/jr/src/nixpkgs/lib\"\nnix-repl&gt; ./lib/..             # nix removes all `..` to avoid redundant path resolutions\n/home/jr/src/nixpkgs\nnix-repl&gt; :q\n</code></pre>\n<pre><code class=\"language-bash\">realpath ./lib/..\n/home/jr/src/nixpkgs\nln -s pkgs/applications lib-symlink\nrealpath ./lib-symlink/..\n/home/jr/src/nixpkgs/pkgs\nnix repl\nnix-repl&gt; ./lib-symlink/..   # Nix doesn't read this file at all like realpath did\n/home/jr/src/nixpkgs\nnix-repl&gt; builtins.readDir ./. # listing of all entries in current dir and their types\n{\n  \".devcontainer\" = \"directory\";\n  \".editorconfig\" = \"regular\";\n  \".git\" = \"directory\";\n  \".git-blame-ignore-revs\" = \"regular\";\n  \".gitattributes\" = \"regular\";\n  \".github\" = \"directory\";\n  \".gitignore\" = \"regular\";\n  \".mailmap\" = \"regular\";\n  \".mergify.yml\" = \"regular\";\n  \".version\" = \"symlink\";\n  \"CONTRIBUTING.md\" = \"regular\";\n  COPYING = \"regular\";\n  \"README.md\" = \"regular\";\n  ci = \"directory\";\n  \"default.nix\" = \"regular\";\n  doc = \"directory\";\n  \"flake.nix\" = \"regular\";\n  lib = \"directory\";\n  maintainers = \"directory\";\n  nixos = \"directory\";\n  pkgs = \"directory\";\n  \"shell.nix\" = \"regular\";\n}\nnix-repl&gt; builtins.readFile ./default.nix\n\"let\\n  requiredVersion = import ./lib/minver.nix;\\nin\\n\\nif !builtins ? nixVersion\n || builtins.compareVersions requiredVersion builtins.nixVersion == 1 then\\n\\n  abort\n ''\\n\\n    This version of Nixpkgs requires Nix &gt;= \\${requiredVersion}, please\n upgrade:\\n\\n    - If you are running NixOS, `nixos-rebuild' can be used to upgrade\n your system.\\n\\n    - Alternatively, with Nix &gt; 2.0 `nix upgrade-nix' can be used\n to imperatively\\n      upgrade Nix. You may use `nix-env --version' to check which\n version you have.\\n\\n    - If you installed Nix using the install script (https://nixos.org/nix/install),\\n\n  it is safe to upgrade by running it again:\\n\\n          curl -L https://nixos.org/nix/install | sh\\n\\n\nFor more information, please see the NixOS release notes at\\n    https://nixos.org/nixos/manual\n or locally at\\n    \\${toString ./nixos/doc/manual/release-notes}.\\n\\n    If you need further help,\n see https://nixos.org/nixos/support.html\\n  ''\\n\\nelse\\n\\n  import ./pkgs/top-level/impure.nix\\n\"\nnix-repl&gt; :l &lt;nixpkgs/lib&gt;\nnix-repl&gt; importJSON ./pkgs/development/python-modules/notebook/missing-hashes.json # Return the nix value for JSON\n{\n  \"@nx/nx-darwin-arm64@npm:16.10.0\" = \"aabcc8499602b98c9fc3b768fe46dfd4e1b818caa84b740bd4f73a2e4528c719b979ecb1c10a0d793a1fead83073a08bc86417588046aa3e587e80af880bffd3\";\n  \"@nx/nx-darwin-x64@npm:16.10.0\" = \"9dd20f45f646d05306f23f5abb7ade69dcb962e23a013101e93365847722079656d30a19c735fdcfa5c4e0fdf08691f9d621073c58aef2861c26741ff4638375\";\n  \"@nx/nx-freebsd-x64@npm:16.10.0\" = \"35b93aabe3b3274d53157a6fc10fec7e341e75e6818e96cfbc89c3d5b955d225ca80a173630b6aa43c448c6b53b23f06a2699a25c0c8bc71396ee20a023d035f\";\n  \"@nx/nx-linux-arm-gnueabihf@npm:16.10.0\" = \"697b9fa4c70f84d3ea8fe32d47635864f2e40b0ceeb1484126598c61851a2ec34b56bb3eeb9654c37d9b14e81ce85a36ac38946b4b90ca403c57fe448be51ccb\";\n  \"@nx/nx-linux-arm64-gnu@npm:16.10.0\" = \"001e71fedfc763a4dedd6c5901b66a4a790d388673fb74675235e19bb8fe031ff3755568ed867513dd003f873901fabda31a7d5628b39095535cb9f6d1dc7191\";\n  \"@nx/nx-linux-arm64-musl@npm:16.10.0\" = \"58e3b71571bdadd2b0ddd24ea6e30cd795e706ada69f685403412c518fba1a2011ac8c2ac46145eab14649aa5a78e0cedcdb4d327ccb3b6ec12e055171f3840b\";\n  \"@nx/nx-linux-x64-gnu@npm:16.10.0\" = \"97729a7efb27301a67ebf34739784114528ddb54047e63ca110a985eaa0763c5b1ea7c623ead1a2266d07107951be81e82ffa0a30e6e4d97506659303f2c8c78\";\n  \"@nx/nx-linux-x64-musl@npm:16.10.0\" = \"442bdbd5e61324a850e4e7bd6f54204108580299d3c7c4ebcec324da9a63e23f48d797a87593400fc32af78a3a03a3c104bfb360f107fe732e6a6c289863853a\";\n  \"@nx/nx-win32-arm64-msvc@npm:16.10.0\" = \"b5c74184ebfc70294e85f8e309f81c3d40b5cf99068891e613f3bef5ddb946bef7c9942d9e6c7688e22006d45d786342359af3b4fc87aadf369afcda55c73187\";\n  \"@nx/nx-win32-x64-msvc@npm:16.10.0\" = \"c5b174ebd7a5916246088e17d3761804b88f010b6b3f930034fa49af00da33b6d1352728c733024f736e4c2287def75bafdc3d60d8738bd24b67e9a4f11763f8\";\n}\nnix-repl&gt; builtins.toJSON  # serialize\n«primop toJSON»\nnix-repl&gt; builtins.fromTOML\n«primop fromTOML»\nnix-repl&gt; builtins.toXML\n</code></pre>\n<p>For more serialization formats see <code>nixpkgs/lib/generators.nix</code> as well as in\n<code>nixpkgs/pkgs/pkgs-lib/formats/</code> we can see them with the <code>nix repl</code> as follows:</p>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs\nnix repl\nnix-repl&gt; :l .\nnix-repl&gt; lib.generators.toYAML {} { a = 10; }\n\"{\\\"a\\\":10}\"\nnix-repl&gt; lib.generators.toYAML {} { a.b.c = 10; }\n\"{\\\"a\\\":{\\\"b\\\":{\\\"c\\\":10}}}\"\nnix-repl&gt; builtins.trace (lib.generators.toYAML {} { a.b.c = 10; }) null\ntrace: {\"a\":{\"b\":{\"c\":10}}}\nnull\nnix-repl&gt; yamlFormat = pkgs.formats.yaml {}\n\nnix-repl&gt; yamlFormat\n{\n  generate = «lambda generate @ /home/jr/src/nixpkgs/pkgs/pkgs-lib/formats.nix:111:9»;\n  type = { ... };\n}\n</code></pre>\n<ul>\n<li>We can see that it provides a <code>generate</code> function that we can use. <code>generate</code>\ndoesn’t just generate a string anymore because if we want to lift the\nrestriction at evaluation time we can’t return the formatted form at\nevaluation time anymore. We need a name to return a derivation continued\nbelow:</li>\n</ul>\n<pre><code class=\"language-bash\">yamlFormat.generate \"name\" { a.b.c = 10; }\n«derivation /nix/store/xakajb2rzbmqqkjbh08bxwqdf0xqvjly-name.drv»\nnix-repl&gt; :b yamlFormat.generate \"name\" { a.b.c = 10; }\nThis derivation produced the following outputs:\nout -&gt; /nix/store/y4c5029k6w3l0qmdw7cq396zrdy5x8yj-name\nnix-repl&gt; :q\n</code></pre>\n<p>Let’s cat the result to see if it’s formatted correctly as YAML:</p>\n<pre><code class=\"language-bash\">cat /nix/store/y4c5029k6w3l0qmdw7cq396zrdy5x8yj-name\n───────┬───────────────────────────────────────────────────────────────\n       │ File: /nix/store/y4c5029k6w3l0qmdw7cq396zrdy5x8yj-name\n───────┼──────────────────────────────────────────────────────────────\n   1   │ a:\n   2   │   b:\n   3   │     c: 10\n───────┴───────────────────────────────────────────────────────────\n</code></pre>\n<p>Looks good. There is also a <code>type</code>:</p>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; :l .\nnix-repl&gt; yamlFormat = pkgs.format.yaml {}\nnix-repl&gt; yamlFormat.type\n{\n  _type = \"option-type\";\n  check = «lambda check @ /home/jr/src/nixpkgs/lib/types.nix:1029:19»;\n  deprecationMessage = null;\n  description = \"YAML value\";\n  descriptionClass = \"conjunction\";\n  emptyValue = { ... };\n  functor = { ... };\n  getSubModules = null;\n  getSubOptions = «lambda @ /home/jr/src/nixpkgs/lib/types.nix:214:25»;\n  merge = «lambda merge @ /home/jr/src/nixpkgs/lib/types.nix:1031:13»;\n  name = \"nullOr\";\n  nestedTypes = { ... };\n  substSubModules = «lambda substSubModules @ /home/jr/src/nixpkgs/lib/types.nix:1046:29»;\n  typeMerge = «lambda defaultTypeMerge @ /home/jr/src/nixpkgs/lib/types.nix:115:10»;\n}\nnix-repl&gt; lib.modules.mergeDefinitions [] yamlFormat.type [ { value = null; } ]\n{\n  defsFinal = [ ... ];\n  defsFinal' = { ... };\n  isDefined = true;\n  mergedValue = null;\n  optionalValue = { ... };\n}\nnix-repl&gt; (lib.modules.mergeDefinitions [] yamlFormat.type [ { value = null; } ]).mergedValue\nnull\nnix-repl&gt; :p (lib.modules.mergeDefinitions [] yamlFormat.type [ { value = { a.b.c = 10; }; } ]).mergedValue\n{\n  a = {\n    b = { c = 10; };\n  };\n}\nnix-repl&gt; :p (lib.modules.mergeDefinitions [] yamlFormat.type [ { value = { a.b.c = 10; }; } { value = { a.b.d = 20; }; } ]).mergedValue\n{\n  a = {\n    b = {\n      c = 10;\n      d = 20;\n    };\n  };\n}\n</code></pre>\n<ul>\n<li><code>lib</code> can’t access any packages, it is entirely at evaluation time. It can’t\naccess any formatters or things like that. If we lift that restriction as is\ndone in <code>pkgs.formats</code> we can make it look much nicer.</li>\n</ul>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs\nnix-build -A hello\nwarning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring\nthis path will be fetched (0.06 MiB download, 0.26 MiB unpacked):\n  /nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\ncopying path '/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2' from 'https://cache.nixos.org'...\n/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\n</code></pre>\n<p>Say we rely on this store path in a derivation:</p>\n<pre><code class=\"language-bash\">nix-repl&gt; thePath = \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\"\nnix-repl&gt; thePath + \"/bin/hello\"\n\"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello\"\n</code></pre>\n<pre><code class=\"language-bash\">hx ~/src/nixpkgs/test2.nix\n</code></pre>\n<pre><code class=\"language-nix\"># test2.nix\nwith import ./. {};\n\nrunCommand \"test\" {\n    nativeBuildInputs = [\n        hello\n    ];\n}''\n  hello &gt; $out\n''\n</code></pre>\n<p>Try building it:</p>\n<pre><code class=\"language-bash\">nix-build test2.nix &amp;&amp; cat result\nwarning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring\n/nix/store/m55p4vpb8s7s28s20vs89i467kxbrdac-test\nHello, world!\n</code></pre>\n<p>Now if we try it with the store path:</p>\n<pre><code class=\"language-nix\"># test2.nix\nwith import ./. {};\n\nrunCommand \"test\" {\n}''\n  /nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello &gt; $out\n''\n</code></pre>\n<p>This doesn’t work</p>\n<pre><code class=\"language-bash\">nix-build test2.nix\nlast 1 log lines:\n&gt; /build/.attr-0l2nkwhif96f51f4amnlf414lhl4rv9vh8iffyp431v6s28gsr90: line 1: /nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello: No such file or directory\nFor full logs, run:\nnix log /nix/store/58zcp9xwgf1sirmzf9sh61j8gz9lkw34-test.drv\nnix-instantiate test2.nix\n/nix/store/58zcp9xwgf1sirmzf9sh61j8gz9lkw34-test.drv\nnix derivation show /nix/store/58zcp9xwgf1sirmzf9sh61j8gz9lkw34-test.drv | jq\n{\n  \"/nix/store/58zcp9xwgf1sirmzf9sh61j8gz9lkw34-test.drv\": {\n    \"args\": [\n      \"-e\",\n      \"/nix/store/vj1c3wf9c11a0qs6p3ymfvrnsdgsdcbq-source-stdenv.sh\",\n      \"/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh\"\n    ],\n    \"builder\": \"/nix/store/xy4jjgw87sbgwylm5kn047d9gkbhsr9x-bash-5.2p37/bin/bash\",\n    \"env\": {\n      \"__structuredAttrs\": \"\",\n      \"buildCommand\": \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello &gt; $out\\n\",\n      \"buildInputs\": \"\",\n      \"builder\": \"/nix/store/xy4jjgw87sbgwylm5kn047d9gkbhsr9x-bash-5.2p37/bin/bash\",\n      \"cmakeFlags\": \"\",\n      \"configureFlags\": \"\",\n      \"depsBuildBuild\": \"\",\n      \"depsBuildBuildPropagated\": \"\",\n      \"depsBuildTarget\": \"\",\n      \"depsBuildTargetPropagated\": \"\",\n      \"depsHostHost\": \"\",\n      \"depsHostHostPropagated\": \"\",\n      \"depsTargetTarget\": \"\",\n      \"depsTargetTargetPropagated\": \"\",\n      \"doCheck\": \"\",\n      \"doInstallCheck\": \"\",\n      \"enableParallelBuilding\": \"1\",\n      \"enableParallelChecking\": \"1\",\n      \"enableParallelInstalling\": \"1\",\n      \"mesonFlags\": \"\",\n      \"name\": \"test\",\n      \"nativeBuildInputs\": \"\",\n      \"out\": \"/nix/store/ljrkx5midby3j7p4g96d74jrq8f9rpya-test\",\n      \"outputs\": \"out\",\n      \"passAsFile\": \"buildCommand\",\n      \"patches\": \"\",\n      \"propagatedBuildInputs\": \"\",\n      \"propagatedNativeBuildInputs\": \"\",\n      \"stdenv\": \"/nix/store/aq801xbgs98nxx3lckrym06qfvl8mfsf-stdenv-linux\",\n      \"strictDeps\": \"\",\n      \"system\": \"x86_64-linux\"\n    },\n    \"inputDrvs\": {\n      \"/nix/store/bmncp7arkdhrl6nkyg0g420935x792gl-stdenv-linux.drv\": {\n        \"dynamicOutputs\": {},\n        \"outputs\": [\n          \"out\"\n        ]\n      },\n      \"/nix/store/rfkzz952hz2d58d90mscxvk87v5wa5bz-bash-5.2p37.drv\": {\n        \"dynamicOutputs\": {},\n        \"outputs\": [\n          \"out\"\n        ]\n      }\n    },\n    \"inputSrcs\": [\n      \"/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh\",\n      \"/nix/store/vj1c3wf9c11a0qs6p3ymfvrnsdgsdcbq-source-stdenv.sh\"\n    ],\n    \"name\": \"test\",\n    \"outputs\": {\n      \"out\": {\n        \"path\": \"/nix/store/ljrkx5midby3j7p4g96d74jrq8f9rpya-test\"\n      }\n    },\n    \"system\": \"x86_64-linux\"\n  }\n}\n</code></pre>\n<p>You see the <code>\"inputDrvs\"</code>, they are the derivations that we depend on and it\ndoesn’t know about the <code>hello.drv</code>. In Nix for the builder sandbox it creates a\nsandbox that only contains the derivations that you depend on which ensures that\nyou can’t depend on any derivation that you haven’t explicitly decalred.</p>\n<p>Nix does have <code>builtins.storePath</code> that allows you to do this, otherwise it’s\nkind of an anti pattern.</p>\n<pre><code class=\"language-nix\"># test2.nix\n# test2.nix\n# test2.nix\nwith import ./. {};\n  runCommand \"test\" {\n  } ''\n    ${builtins.storePath \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\"}/bin/hello &gt; $out\n  ''\n</code></pre>\n<p><code>builtins.storePath</code>: Turns a store path into the thing that it represents in\nthe store.</p>\n<pre><code class=\"language-bash\">nix-build test2.nix &amp;&amp; cat result\n/nix/store/x48741w0k9hgqywzv6wc7rk90r1y75js-test\nHello, world!\n</code></pre>\n<p>To demonstrate what <code>builtins.storePath</code> does:</p>\n<pre><code class=\"language-bash\">nix-repl&gt; builtins.storePath \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello\"\n\"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2/bin/hello\"\nnix-repl&gt; builtins.getContext \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\"\n{ }\nnix-repl&gt; builtins.getContext (builtins.storePath \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\")\n{\n  \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\" = { ... };\n}\nnix-repl&gt; :p builtins.getContext (builtins.storePath \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\")\n{\n  \"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\" = { path = true; };\n}\n</code></pre>\n<pre><code class=\"language-bash\">nix-repl&gt; :l .\nwarning: Nix search path entry '/nix/var/nix/profiles/per-user/root/channels' does not exist, ignoring\nAdded 24878 variables.\n\nnix-repl&gt; hello.outPath\n# this is the output path of the hello derivation\n\"/nix/store/29mhfr5g4dsv07d80b7n4bgs45syk3wl-hello-2.12.2\"\nnix-repl&gt; :p builtins.getContext hello.outPath\n# we see that this is a `.drv`, this is because derivations can have multiple outputs\n{\n  \"/nix/store/ljxsxdy1syy03b9kfnnh8x7zsk21fdcq-hello-2.12.2.drv\" = {\n    outputs = [ \"out\" ];\n  };\n}\n# for example\nnix-repl&gt; openssl.outputs\n[\n  \"bin\"\n  \"dev\"\n  \"out\"\n  \"man\"\n  \"doc\"\n  \"debug\"\n]\nnix-repl&gt; openssl.all\n# a list of all the derivations\n[\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n  «derivation /nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv»\n]\nnix-repl&gt; lib.concatStringsSep \" \" openssl.all\n\"/nix/store/rjzx8v679rwd6dsb6s08iy3j2rrax72s-openssl-3.4.1-bin /nix/store/kcgqglb4iax0zh5jlrxmjdik93wlgsrq-openssl-3.4.1-dev /nix/store/8pviily4fgsl02ijm65binz236717wfs-openssl-3.4.1 /nix/store/1l5b31cnswnbcdcac9rzs9xixnc2n9r5-openssl-3.4.1-man /nix/store/9fz5qmj0z70cbzy7mapml0sbi8z6ap0a-openssl-3.4.1-doc /nix/store/yk2g2gfcj2fy1ffyi1g91q7jmp4h8pxa-openssl-3.4.1-debug\"\nnix-repl&gt; :p builtins.getContext (builtins.unsafeDiscardOutputDependency (lib.concatStringsSep \" \" openssl.all))\n{\n  \"/nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv\" = {\n    outputs = [\n      \"bin\"\n      \"debug\"\n      \"dev\"\n      \"doc\"\n      \"man\"\n      \"out\"\n    ];\n  };\n}\nnix-repl&gt; :p builtins.getContext openssl.drvPath\n{\n  \"/nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv\" = { allOutputs = true; };\n}\n# useful if you need to create a derivation that copies this derivation to another machine\n# remote builders usually take care of this but you may need it occasionally\nnix-repl&gt; :p builtins.getContext (builtins.unsafeDiscardOutputDependency openssl.drvPath)\n{\n  \"/nix/store/rw3y8k94ib37dc86n0wivr551wyzxgsk-openssl-3.4.1.drv\" = { path = true; };\n}\n</code></pre>\n<p>Relying on paths outside of the nix store is generally not recommended because\nof garbage collection and it’s considered unsafe.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/functions/practical_functions_2.1.html",
      "url": "https://saylesss88.github.io/functions/practical_functions_2.1.html",
      "title": "Practical Nix Functions",
      "content_html": "<h1>Practical Nix Functions</h1>\n<details>\n<summary>\n✔️\nIf you want to follow along with this example you'll have to place the following\nin your project directory. Section is collapsed to focus on functions:\n</summary>\n<p><img src=\"https://saylesss88.github.io/images/coding6.png\" alt=\"coding6\" /></p>\n<ol>\n<li>\n<p><a href=\"https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/2.49.3/graphviz-2.49.3.tar.gz\">graphviz</a></p>\n</li>\n<li>\n<p><a href=\"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\">hello</a></p>\n</li>\n<li>\n<p><code>autotools.nix</code>:</p>\n</li>\n</ol>\n<pre><code class=\"language-nix\"># autotools.nix\npkgs: attrs:\nwith pkgs; let\n  defaultAttrs = {\n    builder = \"${bash}/bin/bash\";\n    args = [./builder.sh];\n    setup = ./setup.sh;\n    baseInputs = [gnutar gzip gnumake gcc binutils-unwrapped coreutils gawk gnused gnugrep patchelf findutils];\n    buildInputs = [];\n    system = builtins.currentSystem;\n  };\nin\n  derivation (defaultAttrs // attrs)\n</code></pre>\n<ol start=\"4\">\n<li><code>setup.sh</code>:</li>\n</ol>\n<pre><code class=\"language-bash\"># setup.sh (This is a library of functions setting up the environment, not directly executable)\nunset PATH\nfor p in $baseInputs $buildInputs; do\n  if [ -d $p/bin ]; then\n    export PATH=\"$p/bin${PATH:+:}$PATH\"\n  fi\n  if [ -d $p/lib/pkgconfig ]; then\n    export PKG_CONFIG_PATH=\"$p/lib/pkgconfig${PKG_CONFIG_PATH:+:}$PKG_CONFIG_PATH\"\n  fi\ndone\n\nfunction unpackPhase() {\n  tar -xzf $src\n\n  for d in *; do\n    if [ -d \"$d\" ]; then\n      cd \"$d\"\n      break\n    fi\n  done\n}\n\nfunction configurePhase() {\n  ./configure --prefix=$out\n}\n\nfunction buildPhase() {\n  make\n}\n\nfunction installPhase() {\n  make install\n}\n\nfunction fixupPhase() {\n  find $out -type f -exec patchelf --shrink-rpath '{}' \\; -exec strip '{}' \\; 2&gt;/dev/null\n}\n\nfunction genericBuild() {\n  unpackPhase\n  configurePhase\n  buildPhase\n  installPhase\n  fixupPhase\n}\n</code></pre>\n<ol start=\"5\">\n<li>And finally <code>builder.sh</code>:</li>\n</ol>\n<pre><code class=\"language-bash\"># builder.sh (This is the actual builder script specified in the derivation and\n# what `nix-build` expects)\nset -e\nsource $setup\ngenericBuild\n</code></pre>\n</details>\n<p>This is another example from the Nix-Pill series shown in another way to show\nsome powerful aspects of functions.</p>\n<p>If you have a <code>default.nix</code> like this:</p>\n<pre><code class=\"language-nix\"># default.nix\n{\n  hello = import ./hello.nix;\n  graphviz = import ./graphviz.nix;\n}\n</code></pre>\n<p>It expects the files that it imports to look like this:</p>\n<pre><code class=\"language-nix\"># graphviz.nix\nlet\n  pkgs = import &lt;nixpkgs&gt; { };\n  mkDerivation = import ./autotools.nix pkgs;\nin\nmkDerivation {\n  name = \"graphviz\";\n  src = ./graphviz-2.49.3.tar.gz;\n}\n</code></pre>\n<p>And <code>hello.nix</code>:</p>\n<pre><code class=\"language-nix\"># hello.nix\nlet\n  pkgs = import &lt;nixpkgs&gt; { };\n  mkDerivation = import ./autotools.nix pkgs;\nin\nmkDerivation {\n  name = \"hello\";\n  src = ./hello-2.12.1.tar.gz;\n}\n</code></pre>\n<p>You would build these with:</p>\n<pre><code class=\"language-bash\">nix-build -A hello\nnix-build -A graphviz\n</code></pre>\n<p>As you can see both derivations are dependendent on <code>nixpkgs</code> which they\n<strong>both</strong> import directly. To centralize our dependencies and avoid redundant\nimports, we’ll refactor our individual package definitions (<code>hello.nix</code>,\n<code>graphviz.nix</code>) into functions. Our <code>default.nix</code> will then be responsible for\nsetting up the common inputs (like <code>pkgs</code> and <code>mkDerivation</code>) and passing them\nas arguments when it imports and calls these package functions.</p>\n<p>Here is what our <code>default.nix</code> will look like:</p>\n<pre><code class=\"language-nix\">let\n  pkgs = import &lt;nixpkgs&gt; { };\n  mkDerivation = import ./autotools.nix pkgs;\nin\nwith pkgs;\n{\n  hello = import ./hello.nix { inherit mkDerivation; };\n  graphviz = import ./graphviz.nix {\n    inherit\n      mkDerivation\n      lib\n      gd\n      pkg-config\n      ;\n  };\n  graphvizCore = import ./graphviz.nix {\n    inherit\n      mkDerivation\n      lib\n      gd\n      pkg-config\n      ;\n    gdSupport = false;\n  };\n}\n</code></pre>\n<p>We define some local variables in the <code>let</code> expression and pass them around.</p>\n<p>The whole expression in the above <code>default.nix</code> returns an attribute set with\nthe keys <code>hello</code>, <code>graphviz</code>, and <code>graphvizCore</code></p>\n<p>We import <code>hello.nix</code> and <code>graphviz.nix</code>, which both return a function. We call\nthe functions, passing them a set of inputs with the <code>inherit</code> construct.</p>\n<p>Let’s change <code>hello.nix</code> into a function to match what the <code>default.nix</code> now\nexpects.</p>\n<pre><code class=\"language-nix\"># hello.nix\n{mkDerivation}:\nmkDerivation {\n  name = \"hello\";\n  src = ./hello-2.12.1.tar.gz;\n}\n</code></pre>\n<p>Now our <code>graphviz</code> attribute expects <code>graphviz.nix</code> to be a function that takes\nthe arguments listed in the above <code>default.nix</code>, here’s what <code>graphviz.nix</code> will\nlook like as a function:</p>\n<pre><code class=\"language-nix\"># graphviz.nix\n{\n  mkDerivation,\n  lib,\n  gdSupport ? true,\n  gd,\n  pkg-config,\n}:\nmkDerivation {\n  name = \"graphviz\";\n  src = ./graphviz-2.49.3.tar.gz;\n  buildInputs =\n    if gdSupport\n    then [\n      pkg-config\n      (lib.getLib gd)\n      (lib.getDev gd)\n    ]\n    else [];\n}\n</code></pre>\n<p>We factorized the import of <code>nixpkgs</code> and <code>mkDerivation</code>, and also added a\nvariant of <code>graphviz</code> with gd support disabled. The result is that both\n<code>hello.nix</code> and <code>graphviz.nix</code> are independent of the repository and\ncustomizable by passing specific inputs.</p>\n<p>Now, we can build the package with <code>gd</code> support disabled with the <code>graphvizCore</code>\nattribute:</p>\n<pre><code class=\"language-bash\">nix-build -A graphvizCore\n# or we can still build the package that now defaults to gd support\nnix-build -A graphviz\n</code></pre>\n<p>This example showed us how to turn expressions into functions. We saw how\nfunctions are passed around and shared between Nix expressions and derivations.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/overlays_4.5.html",
      "url": "https://saylesss88.github.io/flakes/overlays_4.5.html",
      "title": "Overlays",
      "content_html": "<h1>Extending Flakes with Custom Packages using Overlays</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/../images/pokego.png\" alt=\"Pokego Logo\" />–<a href=\"https://github.com/rubiin/pokego\">pokego repo</a></p>\n<p>Overlays are Nix functions that accept two arguments, <code>final</code> and <code>prev</code> and\nreturn a set of packages. Overlays are similar to <code>packageOverrides</code> as a way to\ncustomize Nixpkgs, <code>packageOverrides</code> acts as an overlay with only the <code>prev</code>\nargument. Therefore, <code>packageOverrides</code> is appropriate for basic use, but\noverlays are more powerful and easier to distribute.</p>\n<p>Example:</p>\n<pre><code class=\"language-nix\">final: prev: {\n  firefox = prev.firefox.overrideAttrs (old: {\n    buildInputs = (old.buildInputs or []) ++ [ prev.vlc ];\n    env.FIREFOX_DISABLE_GMP_UPDATER = \"1\";\n  });\n}\n</code></pre>\n<p>To see the original derivation, run <code>nix edit -f \"&lt;nixpkgs&gt;\" firefox</code>.</p>\n<p>This modifies Firefox by:</p>\n<ul>\n<li>\n<p>Adding <code>vlc</code> to <code>buildInputs</code>, useful if a package requires additional\ndependencies.</p>\n</li>\n<li>\n<p>Setting an environment variable (<code>FIREFOX_DISABLE_GMP_UPDATER=1</code>) to disable\nautomatic updates of the Gecko Media Plugin.</p>\n</li>\n</ul>\n<p>It is very common to use overlays in Nix to install packages that aren’t\navailable in the standard Nixpkgs repository.</p>\n<p><strong>Overlays</strong> are one of the primary and recommended ways to extend and customize\nyour Nix environment. It’s important to remember that Nix overlays are made to\nallow you to modify or extend the package set provided by Nixpkgs (or other Nix\nsources) without directly altering the original package definitions. This is\ncrucial for maintaining reproducibility and avoiding conflicts. Overlays are\nessentially functions that take the previous package set and allow you to add,\nmodify, or remove packages.</p>\n<ul>\n<li>To better understand the structure of my <code>flake.nix</code> it may be helpful to\nfirst read <a href=\"https://tsawyer87.github.io/posts/nix_flakes_tips/\">This</a> blog\npost first.</li>\n</ul>\n<h2>Adding the overlays output to your Flake</h2>\n<p>I’ll show the process of adding the <code>pokego</code> package that is not in Nixpkgs:</p>\n<ol>\n<li>In my <code>flake.nix</code> I have a custom inputs variable within my let block of my\nflake like so just showing the necessary parts for brevity:</li>\n</ol>\n<pre><code class=\"language-nix\"># flake.nix\n  outputs = my-inputs @ {\n    self,\n    nixpkgs,\n    treefmt-nix,\n    ...\n  }: let\n    system = \"x86_64-linux\";\n    host = \"magic\";\n    userVars = {\n      username = \"jr\";\n      gitUsername = \"saylesss88\";\n      editor = \"hx\";\n      term = \"ghostty\";\n      keys = \"us\";\n      browser = \"firefox\";\n      flake = builtins.getEnv \"HOME\" + \"/flake\";\n    };\n\n    inputs =\n      my-inputs\n      // {\n        pkgs = import inputs.nixpkgs {\n          inherit system;\n        };\n        lib = {\n          overlays = import ./lib/overlay.nix;\n          nixOsModules = import ./nixos;\n          homeModules = import ./home;\n          inherit system;\n        };\n      };\n      # ... snip ...\n</code></pre>\n<ul>\n<li>Why I Created <code>inputs.lib</code> in My <code>flake.nix</code>. In the above example, you’ll\nnotice a <code>lib</code> attribute defined within the main <code>let</code> block.\n<ul>\n<li>\n<p>This might seem a bit unusual at first, as inputs are typically defined at\nthe top level of a flake. However, this structure provides a powerful way to\norganize and reuse common Nix functions and configurations across my flake.</p>\n</li>\n<li>\n<p>By bundling my custom logic and modules into <code>inputs.lib</code>, I can pass\n<code>inputs</code> (which now includes my custom <code>lib</code>) as a <code>specialArgs</code> to other\nmodules. This provides a clean way for all modules to access these shared\nresources. For example, in <code>configuration.nix</code>, <code>inputs.lib.overlays</code>\ndirectly references my custom overlay set.</p>\n</li>\n<li>\n<p>My <code>inputs.lib</code> is my own project-specific library, designed to hold\nfunctions and attribute sets relevant to my flake’s custom configurations.\nWhile <code>nixpkgs.lib</code> is globally available, my custom <code>lib</code> contains my\nunique additions.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>While defining <code>inputs</code> within the <code>let</code> block to achieve this structure is a\npersonal preference and works well for my setup, the core benefit is the\ncreation of a dedicated, centralized <code>lib</code> attribute that encapsulates my\nflake’s reusable Nix code, leading to a more organized and maintainable\nconfiguration.</p>\n<h2>The Actual Overlay</h2>\n<ol start=\"2\">\n<li>In the <code>overlay.nix</code> I have this helper function and the defined package:</li>\n</ol>\n<pre><code class=\"language-nix\"># overlay.nix\n_final: prev: let\n  # Helper function to import a package\n  callPackage = prev.lib.callPackageWith (prev // packages);\n\n  # Define all packages\n  packages = {\n    # Additional packages\n    pokego = callPackage ./pac_defs/pokego.nix {};\n  };\nin\n  packages\n</code></pre>\n<ol>\n<li><code>_final: prev:</code>: This is the function definition of the overlay.</li>\n</ol>\n<ul>\n<li>\n<p><code>_final</code>: This argument represents the final, merged package set after all\noverlays have been applied. It’s often unused within a single overlay, hence\nthe <code>_</code> prefix (a Nix convention for unused variables).</p>\n</li>\n<li>\n<p><code>prev</code>: This is the crucial argument. It represents the package set before\nthis overlay is applied. This allows you to refer to existing packages and\nfunctions from Nixpkgs.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li>\n<p><code>let ... in packages</code>: This introduces a <code>let</code> expression, which defines\nlocal variables within the scope of this overlay function. The <code>in packages</code>\npart means that the overlay function will ultimately return the <code>packages</code>\nattribute set defined within the <code>let</code> block.</p>\n</li>\n<li>\n<p><code>callPackage = prev.lib.callPackageWith (prev // packages)</code>: This line\ndefines a helper function called <code>callPackage</code>.</p>\n</li>\n</ol>\n<ul>\n<li>\n<p><code>prev.lib.callPackageWith</code> Is a function provided by Nixpkgs’ <code>lib</code>.\n<code>callPackageWith</code> is like <code>prev.lib.callPackage</code>, but allows the passing of\nadditional arguments that will then be passed to the package definition.</p>\n</li>\n<li>\n<p><code>(prev // packages)</code>: This is an attribute set merge operation. It takes the\n<code>prev</code> package set (Nixpkgs before this overlay) and merges it with the\n<code>packages</code> attribute set defined later in this overlay.</p>\n</li>\n<li>\n<p>By using <code>callPackageWith</code> with this merged attribute set, the <code>callPackage</code>\nfunction defined here is set up to correctly import package definitions,\nensuring they have access to both the original Nixpkgs and any other packages\ndefined within this overlay.</p>\n</li>\n</ul>\n<ol start=\"4\">\n<li>\n<p><code>packages = { ... };</code>: This defines an attribute set named <code>packages</code>. This\nset will contain all the new or modified packages introduced by this overlay.</p>\n</li>\n<li>\n<p><code>pokego = callPackages ./pac_defs/pokego.nix { };</code>: This is the core of how\nthe <code>pokego</code> package is added.</p>\n</li>\n</ol>\n<ul>\n<li>\n<p><code>pokego =</code>: This defines a new attribute named <code>pokego</code> within the packages\nattribute set. This name will be used to refer to the pokego package later.</p>\n</li>\n<li>\n<p><code>callPackage ./pac_defs/pokego.nix {}</code>: This calls the callPackage helper\nfunction defined earlier.</p>\n</li>\n<li>\n<p><code>./pac_defs/pokego.nix</code>: This is the path to another Nix file(<code>pokego.nix</code>)\nthat contains the actual package definition for pokego. This file would define\nhow to fetch, build, and install the pokego software</p>\n</li>\n<li>\n<p><code>{}</code>: This is an empty attribute set passed as additional arguments to the\n<code>pokego.nix</code> package definition. If <code>pokego.nix</code> expected any specific\nparameters (like versions or dependencies), you would provide them here. Since\nit’s empty, it implies pokego.nix either has no required arguments or uses\ndefault values.</p>\n</li>\n</ul>\n<ol start=\"6\">\n<li><code>in packages</code>: As mentioned earlier, the overlay function returns the\npackages attribute set. When this overlay is applied, the packages defined\nwithin this packages set (including pokego) will be added to the overall Nix\npackage set.</li>\n</ol>\n<h2>The pokego Package definition</h2>\n<p>The following is the <code>./pac_defs/pokego.nix</code>:</p>\n<pre><code class=\"language-nix\"># pokego.nix\n{\n  lib,\n  buildGoModule,\n  fetchFromGitHub,\n}:\nbuildGoModule rec {\n  pname = \"pokego\";\n  version = \"0.3.0\";\n\n  src = fetchFromGitHub {\n    owner = \"rubiin\";\n    repo = \"pokego\";\n    rev = \"v${version}\";\n    hash = \"sha256-cFpEi8wBdCzAl9dputoCwy8LeGyK3UF2vyylft7/1wY=\";\n  };\n\n  vendorHash = \"sha256-7SoKHH+tDJKhUQDoVwAzVZXoPuKNJEHDEyQ77BPEDQ0=\";\n\n  # Install shell completions\n  postInstall = ''\n    install -Dm644 completions/pokego.bash \"$out/share/bash-completion/completions/pokego\"\n    install -Dm644 completions/pokego.fish \"$out/share/fish/vendor_completions.d/pokego.fish\"\n    install -Dm644 completions/pokego.zsh \"$out/share/zsh/site-functions/_pokego\"\n  '';\n\n  meta = with lib; {\n    description = \"Command-line tool that lets you display Pokémon sprites in color directly in your terminal\";\n    homepage = \"https://github.com/rubiin/pokego\";\n    license = licenses.gpl3Only;\n    maintainers = with maintainers; [\n      rubiin\n      jameskim0987\n      vinibispo\n    ];\n    mainProgram = \"pokego\";\n    platforms = platforms.all;\n  };\n}\n</code></pre>\n<h2>Adding the overlay to your configuration</h2>\n<p>There are a few places you could choose to put the following, I choose to use my\n<code>configuration.nix</code> because of my setup:</p>\n<pre><code class=\"language-nix\"># configuration.nix\nnixpkgs.overlays = [inputs.lib.overlays]\n</code></pre>\n<h2>Installing Pokego</h2>\n<ul>\n<li>If you are managing your entire system configuration with NixOS, you would\ntypically add <code>pokego</code> to your <code>environment.systemPackages</code>.</li>\n</ul>\n<pre><code class=\"language-nix\"># configuration.nix\nenvironment.systemPackages = with pkgs; [\n  pokego\n]\n</code></pre>\n<ul>\n<li>If you prefer home-manager you can install <code>pokego</code> with home-manager also:</li>\n</ul>\n<pre><code class=\"language-nix\"># home.nix\nhome.packages = [\n  pkgs.pokego\n]\n</code></pre>\n<h3>Another Overlay Example</h3>\n<pre><code class=\"language-nix\">{\n  inputs = {\n    nixpkgs.url = \"https://flakehub.com/NixOS/nixpkgs/*.tar.gz\";\n\n    nix.url = \"https://flakehub.com/f/NixOS/nix/2.17.0.tar.gz\";\n  };\n\n  outputs = { self, nixpkgs, nix }:\n\n    let\n      system = \"aarch64-darwin\";\n      pkgs = import nixpkgs {\n        inherit system;\n        overlays = [\n          nix.overlays.default\n        ];\n      };\n    in\n    {\n     # `pkgs` is nixpkgs for the system, with nix's overlay applied\n    };\n}\n</code></pre>\n<ul>\n<li>\n<p>Normally,\n<code>pkgs = import nixpkgs { }`` imports Nixpkgs with default settings.  However, the example above customizes this import by passing arguments:  </code>pkgs\n= import nixpkgs { inherit system; overlays = [\nnix.overlays.default];}<code>.  This makes the pkgs variable represent nixpkgs specifically for the </code>aarch64-darwin`\nsystem, with the overlay from the nix flake applied.</p>\n</li>\n<li>\n<p>Consequently, any packages built using this customized <code>pkgs</code> will now depend\non or use the specific nix version (<code>2.17.0</code>) provided by the nix flake,\ninstead of the version that comes with the fetched <code>nixpkgs</code>. This technique\ncan be useful for ensuring a consistent environment or testing specific\npackage versions.</p>\n</li>\n</ul>\n<h2>Customizing Nixpkgs Imports and Overlays</h2>\n<p>While overlays are typically used to add or modify packages within a single\n<code>nixpkgs</code> instance, Nix’s lazy evaluation and flake inputs allow for even more\npowerful scenarios. You can have multiple versions of nixpkgs in a single flake,\nand they will only be evaluated when a package from that specific version is\nactually referenced. This complements overlays by giving you fine-grained\ncontrol over which nixpkgs instance an overlay applies to, or which <code>nixpkgs</code>\nversion a specific part of your project depends on.</p>\n<p>Consider this example where we import nixpkgs with a specific overlay applied\ndirectly at the import site:</p>\n<pre><code class=\"language-nix\">{\n  inputs = {\n    nixpkgs.url = \"[https://flakehub.com/NixOS/nixpkgs/*.tar.gz](https://flakehub.com/NixOS/nixpkgs/*.tar.gz)\"; # This will be the base nixpkgs\n\n    nix.url = \"[https://flakehub.com/f/NixOS/nix/2.17.0.tar.gz](https://flakehub.com/f/NixOS/nix/2.17.0.tar.gz)\"; # This flake provides an overlay for a specific Nix version\n  };\n\n  outputs = { self, nixpkgs, nix }:\n\n    let\n      system = \"aarch64-darwin\";\n      # Here, we import nixpkgs and apply the 'nix' flake's overlay.\n      # This 'pkgs' variable now holds a customized Nix package set.\n      # In this 'pkgs' set, the 'nix' package (and anything that depends on it)\n      # will be version 2.17.0 as defined by the 'nix' flake's overlay.\n      pkgs_with_custom_nix = import nixpkgs {\n        inherit system;\n        overlays = [\n          nix.overlays.default # Apply the overlay from the 'nix' flake here\n        ];\n      };\n    in\n    {\n      # We can then expose packages or devShells that use this customized `pkgs` set.\n      devShells.${system}.default = pkgs_with_custom_nix.mkShell {\n        packages = [\n          pkgs_with_custom_nix.nix # This 'nix' package is now version 2.17.0 due to the overlay!\n        ];\n        shellHook = ''\n          echo \"Using Nix version: &lt;span class=\"math-inline\"&gt;\\(nix \\-\\-version\\)\"\n'';\n};\n# You can also make this customized package set available as a top-level overlay\n# if other parts of your flake or configuration want to use it.\n# overlays.custom-nix-version = final: prev: {\n#   inherit (pkgs_with_custom_nix) nix; # Expose the specific nix package from our overlayed pkgs\n# };\n# You can also import multiple versions of nixpkgs and select packages from them:\n# pkgs-2505 = import (inputs.nixpkgs-2505 or nixpkgs) { inherit system; }; # Example, assuming 2505 is an input\n# packages.&lt;/span&gt;{system}.my-tool-2505 = pkgs-2505.myTool; # Using a package from a specific stable version\n    };\n}\n</code></pre>\n<p>Normally, <code>pkgs = import nixpkgs { }</code> imports Nixpkgs with default settings.\nHowever, the example above customizes this import by passing arguments:\n<code>pkgs = import nixpkgs { inherit system; overlays = [ nix.overlays.default];}</code>.\nThis makes the <code>pkgs_with_custom_nix</code> variable represent Nixpkgs specifically\nfor the <code>aarch64-darwin</code> system, with the overlay from the nix flake applied at\nthe time of import.</p>\n<p>Consequently, any packages built using this customized <code>pkgs_with_custom_nix</code>\nwill now depend on or use the specific Nix version (<code>2.17.0</code>) provided by the\nnix flake’s overlay, instead of the version that comes with the base <code>nixpkgs</code>\ninput. This technique is highly useful for ensuring a consistent environment or\ntesting specific package versions without affecting the entire system’s\n<code>nixpkgs</code> set.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/flakes/specialisations_4.6.html",
      "url": "https://saylesss88.github.io/flakes/specialisations_4.6.html",
      "title": "Specialisations",
      "content_html": "<h1>NixOS Specialisations For Multiple Profiles</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><strong>NixOS specialisations</strong> are a powerful feature that allow you to define\nalternative system configurations variations within a single NixOS setup. Each\nspecialisation can modify or extend the base configuration, and NixOS will\ngenerate separate boot entries for each, letting you choose at boot time (or\nswitch at runtime) which environment to use. This is ideal for testing,\nhardware-specific tweaks, or separating work and personal environments without\nmaintaining multiple configuration files</p>\n<h2>How Specialisations Work</h2>\n<p>Specialisations are defined as attributes under the <code>specialisation</code> option in\nyour configuration. Each key (e.g., <code>niri-test</code>) represents a named\nspecialisation, and its configuration attribute contains the NixOS options to\napply on top of the base system</p>\n<p>By default, a specialisation inherits the parent configuration and applies its\nchanges on top. You can also set <code>inheritParentConfig = false;</code> to create a\ncompletely separate configuration.</p>\n<p>After running <code>nixos-rebuild boot</code>, your bootloader will present extra entries\nfor each specialisation. Selecting one boots into the system with that\nspecialisation’s settings applied</p>\n<p>Runtime Switching: You can switch to a specialisation at runtime using\nactivation scripts, e.g.:</p>\n<pre><code class=\"language-bash\">nixos-rebuild switch --specialisation niri-test\n</code></pre>\n<p>or</p>\n<pre><code class=\"language-bash\">/run/current-system/specialisation/niri-test/bin/switch-to-configuration switch\n</code></pre>\n<blockquote>\n<p>Note: Some changes (like kernel switches) require a reboot to take effect</p>\n</blockquote>\n<p>Example: Let’s create a basic specialisation to try out the Niri Window Manager:</p>\n<p>First we have to add the <code>niri-flake</code> as an input to our <code>flake.nix</code> and add the\nmodule to install it:</p>\n<pre><code class=\"language-nix\"># flake.nix\ninputs = {\n     niri.url = \"github:sodiboo/niri-flake\";\n};\n</code></pre>\n<pre><code class=\"language-nix\"># configuration.nix\n{ pkgs, inputs, ... }: {\n# ... snip ...\nimports = [\n    inputs.niri.nixosModules.niri\n];\n\n# This is the top-level overlay\n  nixpkgs.overlays = [inputs.niri.overlays.niri];\n\n# ... snip ...\n\n  specialisation = {\n    niri-test.configuration = {\n      system.nixos.tags = [\"niri\"];\n\n      # Add the Niri overlay for this specialisation\n      nixpkgs.overlays = [inputs.niri.overlays.niri];\n\n      # Enable Niri session\n      programs.niri = {\n        enable = true;\n        package = pkgs.niri-unstable;\n      };\n\n      # Optionally, add a test user and greetd for login\n      users.users.niri = {\n        isNormalUser = true;\n        extraGroups = [\"networkmanager\" \"video\" \"wheel\"];\n        initialPassword = \"test\"; # for testing only!\n        createHome = true;\n      };\n\n      services.greetd = {\n        enable = true;\n        settings = rec {\n          initial_session = {\n            command = lib.mkForce \"${pkgs.niri}/bin/niri\";\n            user = lib.mkForce \"niri\";\n          };\n          default_session = initial_session;\n        };\n      };\n\n      environment.etc.\"niri/config.kdl\".text = ''\n        binds {\n          Mod+T { spawn \"alacritty\"; }\n          Mod+D { spawn \"fuzzel\"; }\n          Mod+Q { close-window; }\n          Mod+Shift+Q { exit; }\n        }\n      '';\n      environment.systemPackages = with pkgs; [\n        alacritty\n        waybar\n        fuzzel\n        mako\n        firefox\n      ];\n\n      programs.firefox.enable = true;\n\n      services.pipewire = {\n        enable = true;\n        alsa.enable = true;\n        pulse.enable = true;\n        # Optionally:\n        jack.enable = true;\n      };\n\n      hardware.alsa.enablePersistence = true;\n\n      networking.networkmanager.enable = true;\n    };\n  };\n}\n</code></pre>\n<p>I chose to use the nightly version so it was required to add the overlay at the\ntop-level as well as inside the <code>specialisation</code> block.</p>\n<p>On my system it sped up build times to first run:</p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --flake .\n# And Then Run\nsudo nixos-rebuild boot --flake .\n</code></pre>\n<p><strong>What this does</strong>:</p>\n<ul>\n<li>\n<p>Creates a boot entry called <code>niri-test</code> with the Niri Wayland compositor, a\ntest user, and a <code>greetd</code> login manager.</p>\n</li>\n<li>\n<p>Installs a set of packages and enables PipeWire with ALSA, PulseAudio, and\nJACK support.</p>\n</li>\n<li>\n<p>Provides a custom Niri configuration file for a few keybinds and enables\nNetworkManager.</p>\n</li>\n</ul>\n<h2>Using Your Specialisation After Boot</h2>\n<p>Once you have rebooted and selected your specialisation from the boot menu, you\ncan use your system as usual. If you want to add or remove programs, change\nsettings, or update your environment within a specialisation, simply:</p>\n<ol>\n<li>\n<p>Edit your configuration: Add or remove packages (e.g., add <code>ghostty</code> to\n<code>environment.systemPackages</code>) or change any other options inside the\nrelevant <code>specialisation</code> block in your NixOS configuration.</p>\n</li>\n<li>\n<p>Apply changes with a rebuild: Run the standard NixOS rebuild command. If you\nare currently running the specialisation you want to update, use:</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch\n</code></pre>\n<p>This will apply your changes to the current specialisation</p>\n<p>If you want to build and activate a different specialisation from your current\nsession, use:</p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --specialisation name\n</code></pre>\n<p>Or, you can activate a specialisation directly with:</p>\n<pre><code class=\"language-bash\">sudo /run/current-system/specialisation/&lt;name&gt;/bin/switch-to-configuration switch\n</code></pre>\n<p>Replace <code>&lt;name&gt;</code> with your specialisation’s name.</p>\n<p>Reboot if needed: Most changes apply immediately, but some (like kernel or\n<code>initrd</code> changes) require a reboot for the specialisation to fully take effect</p>\n<p><strong>Tip</strong>:</p>\n<p>Each specialisation can have its own set of installed programs. Only those\nlisted in the <code>environment.systemPackages</code> (or enabled via modules) inside the\n<code>specialisation</code> block will be available when you boot into that context.</p>\n<p>You manage and update your specialisation just like your main NixOS system no\nspecial commands or workflow are required beyond specifying the specialisation\nwhen rebuilding or switching.</p>\n<h2>Use Cases for Specialisations</h2>\n<ul>\n<li>\n<p><strong>Hardware Profiles</strong>: Enable/disable drivers or services for specific\nhardware (e.g., eGPU, WiFi, or SR-IOV setups)</p>\n</li>\n<li>\n<p><strong>Desktop Environments</strong>: Quickly switch between different desktop\nenvironments or compositors (e.g., GNOME, Plasma, Niri)</p>\n</li>\n<li>\n<p><strong>Testing</strong>: Safely try out unstable packages, new kernels, or experimental\nfeatures without risking your main environment</p>\n</li>\n<li>\n<p><strong>User Separation</strong>: Create profiles for different users, each with their own\nsettings, packages, and auto-login</p>\n</li>\n<li>\n<p><strong>Secure Environments</strong>: Combine with encrypted partitions for more secure,\nisolated setups</p>\n</li>\n</ul>\n<h2>Securely Separated Contexts with NixOS Specialisations</h2>\n<p>I will just explain the concept here for completeness, if you want to implement\nthis I recommend following:</p>\n<p><a href=\"https://www.tweag.io/blog/2022-11-01-hard-user-separation-with-nixos/\">Tweag Hard User Separation with NixOS</a></p>\n<details>\n<summary> ✔️ Click To Expand Section on Separate Contexts </summary>\n<p>If you use the same computer in different contexts such as for work and for your\nprivate life you may worry about the risks of mixing sensitive environments. For\nexample, a cryptolocker received through a compromised work email could\npotentially encrypt your personal files, including irreplaceable family photos.</p>\n<p>A common solution is to install two different operating systems and dual-boot\nbetween them, keeping work and personal data isolated. However, this approach\nmeans you have two systems to maintain, update, and configure, which can be a\nsignificant hassle.</p>\n<p>NixOS offers a third alternative: With NixOS specialisations, you can manage two\n(or more) securely separated contexts within a single operating system. At boot\ntime, you select which context you want to use work or personal. Each context\ncan have its own encrypted root partition, user accounts, and configuration, but\nboth share the same Nix store for packages. This means:</p>\n<ul>\n<li>\n<p>No duplicated packages: Both contexts use the same system-wide package store,\nsaving space and simplifying updates.</p>\n</li>\n<li>\n<p>Single system to maintain: You update and manage only one NixOS installation,\nnot two.</p>\n</li>\n<li>\n<p>Strong security boundaries: Each context can have its own encrypted root, so a\ncompromise in one context (such as malware in your work environment) cannot\naccess the data in the other context.</p>\n</li>\n<li>\n<p>Flexible management: You can configure both contexts from either environment,\nmaking administration easier.</p>\n</li>\n</ul>\n<p>This approach combines the security of dual-booting with the convenience and\nefficiency of a single, unified system.</p>\n<p><strong>How It Works</strong>:</p>\n<ul>\n<li>\n<p>Encrypted Partitions: Each context (work and personal) has its own encrypted\nroot partition. The shared /nix/store partition is also encrypted, but can be\nunlocked by either context.</p>\n</li>\n<li>\n<p>Specialisations at Boot: NixOS generates multiple boot entries, one for each\ncontext. You simply choose your desired environment at boot time.</p>\n</li>\n<li>\n<p>Separation of Data: Your work and personal home directories, settings, and\ndocuments remain isolated from each other, while still benefiting from shared\nsystem packages.</p>\n</li>\n</ul>\n<p>Benefits Over Traditional Dual-Boot</p>\n<ul>\n<li>\n<p>Only one system to update and configure.</p>\n</li>\n<li>\n<p>No wasted disk space on duplicate packages.</p>\n</li>\n<li>\n<p>Seamless switching between contexts with a reboot.</p>\n</li>\n<li>\n<p>Consistent NixOS tooling and workflows in both environments.</p>\n</li>\n</ul>\n<p>What You Need</p>\n<ul>\n<li>\n<p>A physical or virtual machine supported by NixOS.</p>\n</li>\n<li>\n<p>Willingness to erase the system disk during setup.</p>\n</li>\n<li>\n<p>LVM (Logical Volume Manager) support: This setup requires using LVM for disk\npartitioning and management. LVM allows you to create multiple logical volumes\non a single physical disk, making it possible to securely separate your work\nand personal environments while sharing a common Nix store. You will use LVM\ncommands such as <code>pvcreate</code>, <code>vgcreate</code>, and <code>lvcreate</code> to prepare your disk\nlayout</p>\n</li>\n</ul>\n<p>In summary: With NixOS specialisations and careful disk partitioning, you can\nachieve secure, convenient, and efficient context separation—no need to\ncompromise between security and manageability.</p>\n</details>\n<h3>Tips and Best Practices</h3>\n<ul>\n<li>\n<p>Overriding Values: Use <code>lib.mkDefault</code> or <code>lib.mkForce</code> to make options\noverridable or forced in specialisations. I had to do it above because I have\ngreetd setup for my main configuration as well.</p>\n</li>\n<li>\n<p>Selective Configuration: If you want certain options only in the default\n(non-specialised) system, use:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">config = lib.mkIf (config.specialisation != {}) { ... }\n</code></pre>\n<ul>\n<li>\n<p>This condition checks if you’re in a specialisation.</p>\n</li>\n<li>\n<p>Any settings inside this block will <strong>not</strong> be inherited by specialisations,\nkeeping them exclusive to the main system.</p>\n</li>\n<li>\n<p>Runtime Limitations: Not all changes (e.g., kernel or <code>initrd</code>) can be fully\napplied at runtime; a reboot is required for those.</p>\n</li>\n<li>\n<p>Modularity: Specialisations work well with modular NixOS configs keep\nhardware, user, and service configs in separate files for easier management</p>\n</li>\n</ul>\n<p>References to Official Documentation and Community Resources</p>\n<ul>\n<li>\n<p><a href=\"https://www.tweag.io/blog/2022-08-18-nixos-specialisations/\">Tweag: Introduction to NixOS specialisations</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Specialisation\">NixOS Wiki: Specialisation</a></p>\n</li>\n<li>\n<p><a href=\"https://www.tweag.io/blog/2022-11-01-hard-user-separation-with-nixos/\">Tweag Hard User Separation with NixOS</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Comparing_Flakes_and_Traditional_Nix_8.html",
      "url": "https://saylesss88.github.io/Comparing_Flakes_and_Traditional_Nix_8.html",
      "title": "Comparing Flakes and Traditional Nix",
      "content_html": "<h1>Chapter 8</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![nixWinter](images/nixWinter.png) -->\n<h2>Comparing Flakes and Traditional Nix</h2>\n<ul>\n<li>This post is based on notes from Nix-Hour #4, comparing Traditional Nix and\nFlakes, focusing on achieving pure build results. See the\n<a href=\"https://www.youtube.com/watch?v=atmoYyBAhF4\">YouTube video</a> for the original\ncontent. This guide adapts the information for clarity and ease of\nunderstanding.</li>\n</ul>\n<details>\n<summary> What is Purity in Nix? (click here) </summary>\n<ul>\n<li>\n<p>A key benefit of Nix Flakes is their <em>default</em> enforcement of <strong>pure\nevaluation</strong>.</p>\n</li>\n<li>\n<p>In Nix, an <strong>impure operation</strong> depends on something <em>outside</em> its explicit\ninputs. Examples include:</p>\n<ul>\n<li>User’s system configuration</li>\n<li>Environment variables</li>\n<li>Current time</li>\n</ul>\n</li>\n<li>\n<p>Impurity leads to unpredictable builds that may differ across systems or time.</p>\n</li>\n</ul>\n</details>\n<h2>Building a Simple “hello” Package: Flakes vs. Traditional Nix</h2>\n<ul>\n<li>We’ll demonstrate building a basic “hello” package using both Flakes and\nTraditional Nix to highlight the differences in handling purity.</li>\n</ul>\n<h2>Using Nix Flakes</h2>\n<details>\n<summary> Building Hello with Flakes (click here) </summary>\n<ol>\n<li>\n<p><strong>Setup:</strong></p>\n<pre><code class=\"language-bash\">mkdir hello &amp;&amp; cd hello/\n</code></pre>\n</li>\n<li>\n<p><strong>Create <code>flake.nix</code> (Initial Impure Example):</strong></p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  outputs = { self, nixpkgs }: {\n    myHello = (import nixpkgs {}).hello;\n  };\n}\n</code></pre>\n<ul>\n<li>Note: Flakes don’t have access to <code>builtins.currentSystem</code> directly.</li>\n</ul>\n</li>\n<li>\n<p><strong>Impure Build (Fails):</strong></p>\n<pre><code class=\"language-bash\">nix build .#myHello\n</code></pre>\n<ul>\n<li>This fails because Flakes enforce purity by default.</li>\n</ul>\n</li>\n<li>\n<p><strong>Force Impure Build:</strong></p>\n<pre><code class=\"language-bash\">nix build .#myHello --impure\n</code></pre>\n</li>\n<li>\n<p><strong>Making the Flake Pure:</strong></p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in {\n        packages.myHello = pkgs.hello;\n      }\n    );\n}\n</code></pre>\n<ul>\n<li><code>flake-utils</code> simplifies making flakes system-agnostic and provides the\n<code>system</code> attribute.</li>\n</ul>\n</li>\n<li>\n<p><strong>Pure Build (Success):</strong></p>\n<pre><code class=\"language-bash\">nix build .#myHello\n</code></pre>\n</li>\n</ol>\n  </details>\n<h2>Using Traditional Nix</h2>\n<details>\n<summary> Building hello with Traditional Nix </summary>\n<ol>\n<li>\n<p><strong>Setup:</strong></p>\n<pre><code class=\"language-bash\">mkdir hello2 &amp;&amp; cd hello2/\n</code></pre>\n</li>\n<li>\n<p><strong>Create <code>default.nix</code> (Initial Impure Example):</strong></p>\n<pre><code class=\"language-nix\"># default.nix\n{ myHello = (import &lt;nixpkgs&gt; { }).hello; }\n</code></pre>\n</li>\n<li>\n<p><strong>Build (Impure):</strong></p>\n<pre><code class=\"language-bash\">nix-build -A myHello\n</code></pre>\n</li>\n<li>\n<p><strong>Impurity Explained:</strong></p>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; &lt;nixpkgs&gt;\n/nix/var/nix/profiles/per-user/root/channels/nixos\n</code></pre>\n<ul>\n<li><code>&lt;nixpkgs&gt;</code> depends on the user’s environment (Nixpkgs channel), making it\nimpure. Even with channels disabled, it relies on a specific Nixpkgs\nversion in the store.</li>\n</ul>\n</li>\n<li>\n<p><strong>Achieving Purity: Using <code>fetchTarball</code></strong></p>\n<ul>\n<li>\n<p>GitHub allows downloading repository snapshots at specific commits,\ncrucial for reproducibility.</p>\n</li>\n<li>\n<p><strong>Get Nixpkgs Revision from <code>flake.lock</code> (from the Flake example):</strong></p>\n</li>\n</ul>\n<pre><code class=\"language-nix\"># flake.lock\n\"nixpkgs\": {\n  \"locked\": {\n    \"lastModified\": 1746372124,\n    \"narHash\": \"sha256-n7W8Y6bL7mgHYW1vkXKi9zi/sV4UZqcBovICQu0rdNU=\",\n    \"owner\": \"NixOS\",\n    \"repo\": \"nixpkgs\",\n    \"rev\": \"f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0\",\n    \"type\": \"github\"\n  },\n</code></pre>\n</li>\n<li>\n<p><strong>Modify <code>default.nix</code> for Purity:</strong></p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  nixpkgs = fetchTarball {\n    url = \"[https://github.com/NixOS/nixpkgs/archive/f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0.tar.gz](https://github.com/NixOS/nixpkgs/archive/f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0.tar.gz)\";\n    sha256 = \"0000000000000000000000000000000000000000000000000000\"; # Placeholder\n  };\nin {\n  myHello = (import nixpkgs {}).hello;\n}\n</code></pre>\n<ul>\n<li>Replace <code>&lt;nixpkgs&gt;</code> with <code>fetchTarball</code> and a specific revision. A\nplaceholder <code>sha256</code> is used initially.</li>\n</ul>\n</li>\n<li>\n<p><strong>Build (Nix provides the correct <code>sha256</code>):</strong></p>\n<pre><code class=\"language-bash\">nix-build -A myHello\n</code></pre>\n</li>\n<li>\n<p><strong>Verification:</strong> Both Flake and Traditional Nix builds now produce the same\noutput path.</p>\n</li>\n<li>\n<p><strong>Remaining Impurities in Traditional Nix:</strong></p>\n<ul>\n<li>Default arguments to <code>import &lt;nixpkgs&gt; {}</code> can introduce impurity:\n<ul>\n<li><code>overlays</code>: <code>~/.config/nixpkgs/overlays</code> (user-specific)</li>\n<li><code>config</code>: <code>~/.config/nixpkgs/config.nix</code> (user-specific)</li>\n<li><code>system</code>: <code>builtins.currentSystem</code> (machine-specific)</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Making Traditional Nix Fully Pure:</strong></p>\n<pre><code class=\"language-nix\"># default.nix\n{system ? builtins.currentSystem}:\nlet\n  nixpkgs = fetchTarball {\n    url =\n      \"[https://github.com/NixOS/nixpkgs/archive/0243fb86a6f43e506b24b4c0533bd0b0de211c19.tar.gz](https://github.com/NixOS/nixpkgs/archive/0243fb86a6f43e506b24b4c0533bd0b0de211c19.tar.gz)\";\n    sha256 = \"1qvdbvdza7hsqhra0yg7xs252pr1q70nyrsdj6570qv66vq0fjnh\";\n  };\nin {\n  myHello = (import nixpkgs {\n    overlays = [];\n    config = {};\n    inherit system;\n  }).hello;\n}\n</code></pre>\n<ul>\n<li>Override impure defaults for <code>overlays</code>, <code>config</code>, and make <code>system</code> an\nargument.</li>\n</ul>\n</li>\n<li>\n<p><strong>Building with a Specific System:</strong></p>\n<pre><code class=\"language-bash\">nix-build -A myHello --argstr system x86_64-linux\n</code></pre>\n</li>\n<li>\n<p><strong>Pure Evaluation Mode in Traditional Nix:</strong></p>\n<pre><code class=\"language-bash\">nix-instantiate --eval --pure-eval --expr 'fetchGit { url = ./.; rev = \"b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad\"; }'\n</code></pre>\n<ul>\n<li>Example of using <code>--pure-eval</code>.</li>\n</ul>\n<pre><code class=\"language-bash\">nix-build --pure-eval --expr '(import (fetchGit { url = ./.; rev = \"b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad\"; }) { system = \"x86_64-linux\"; }).myHello'\n</code></pre>\n<ul>\n<li>Building with a specific revision and system.</li>\n</ul>\n</li>\n</ol>\n  </details>\n<h3>Updating Nixpkgs</h3>\n<details>\n<summary> Updating Nixpkgs with Flakes </summary>\n<pre><code class=\"language-bash\">nix flake update\n</code></pre>\n<pre><code class=\"language-nix\">nix build .#myHello --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11\n</code></pre>\n</details>\n<h3>Updating Traditional Nix (using <code>niv</code>)</h3>\n<details>\n<summary> Updating with niv </summary>\n<pre><code class=\"language-nix\">nix-shell -p niv\nniv init\n</code></pre>\n<pre><code class=\"language-nix\"># default.nix\n{ system ? builtins.currentSystem,\n  sources ? import nix/sources.nix,\n  nixpkgs ? sources.nixpkgs,\n  pkgs ? import nixpkgs {\n    overlays = [ ];\n    config = { };\n    inherit system;\n  }, }: {\n  myHello = pkgs.hello;\n}\n</code></pre>\n<p>And build it with:</p>\n<pre><code class=\"language-bash\">nix-build -A myHello\n</code></pre>\n<pre><code class=\"language-bash\">niv update nixpkgs --branch=nixos-unstable\nnix-build -A myHello\n</code></pre>\n</details>\n<details>\n<summary> Adding Home-Manager with Flakes (click here) </summary>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n    home-manager.url = \"github:nix-community/home-manager\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils, home-manager, ... }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let pkgs = nixpkgs.legacyPackages.${system};\n      in {\n        packages.myHello = pkgs.hello;\n        packages.x86_64-linux.homeManagerDocs =\n          home-manager.packages.x86_64-linux.docs-html;\n      });\n}\n</code></pre>\n<pre><code class=\"language-bash\">nix flake update\nnix flake show github:nix-community/home-manager\n</code></pre>\n<pre><code class=\"language-nix\">home-manager.inputs.follows = \"nixpkgs\";\n</code></pre>\n</details>\n<h4>Adding Home-Manager with Traditional Nix</h4>\n<details>\n<summary> Adding Home-Manager with Traditional Nix (click here) </summary>\n```nix\nniv add nix-community/home-manager\n```\n<pre><code class=\"language-nix\">nix repl\nnix-repl&gt; s = import ./nix/sources.nix\nnix-repl&gt; s.home-manager\n</code></pre>\n<pre><code class=\"language-nix\">{ system ? builtins.currentSystem, sources ? import nix/sources.nix\n  , nixpkgs ? sources.nixpkgs, pkgs ? import nixpkgs {\n    overlays = [ ];\n    config = { };\n    inherit system;\n  }, }: {\n  homeManagerDocs = (import sources.home-manager { pkgs = pkgs; }).docs;\n\n  myHello = pkgs.hello;\n}\n</code></pre>\n<pre><code class=\"language-bash\">nix-build -A homeManagerDocs\n</code></pre>\n</details>\n<h4>Conclusion</h4>\n<p>In this chapter, we’ve explored the key differences between traditional Nix and\nNix Flakes, particularly focusing on how each approach handles purity,\ndependency management, and project structure. We’ve seen that while traditional\nNix can achieve purity with careful configuration, Flakes enforce it by default,\noffering a more robust and standardized way to build reproducible environments.\nFlakes also streamline dependency management and provide a more structured\nproject layout compared to the often ad-hoc nature of traditional Nix projects.</p>\n<p>However, regardless of whether you’re working with Flakes or traditional Nix,\nunderstanding how to debug and trace issues within your Nix code is crucial.\nWhen things go wrong, you’ll need tools and techniques to inspect the evaluation\nprocess, identify the source of errors, and understand how your modules and\nderivations are being constructed.</p>\n<p>In our next chapter,\n<a href=\"https://saylesss88.github.io/Debugging_and_Tracing_NixOS_Modules_9.html\">Debugging and Tracing Modules</a>,\nwe will delve into the world of Nix debugging. We’ll explore various techniques\nand tools that can help you understand the evaluation process, inspect the\nvalues of expressions, and trace the execution of your Nix code, enabling you to\neffectively troubleshoot and resolve issues in both Flake-based and traditional\nNix projects.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/index.html",
      "url": "https://saylesss88.github.io/installation/index.html",
      "title": "My Chapter",
      "content_html": "<h1>Installation Guides</h1>\n<p>This section provides detailed guides for installing NixOS. You’ll choose\nbetween an <strong>unencrypted</strong> or <strong>encrypted</strong> base setup. After your core\ninstallation, you can explore adding optional features like <code>sops</code> for encrypted\nsecrets, <code>lanzaboote</code> for Secure Boot, or <code>impermanence</code> for a stateless system.</p>\n<hr />\n<h2>1. Unencrypted Disko Btrfs Subvol Installation</h2>\n<ul>\n<li>\n<p><strong>Guide:</strong>\n<a href=\"https://saylesss88.github.io/installation/unenc/unencrypted_setups.html\">Minimal Btrfs-Subvol Install with Disko and Flakes</a></p>\n</li>\n<li>\n<p><strong>Best for:</strong></p>\n<ul>\n<li>\n<p>Users who want a straightforward and quick setup.</p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/installation/unenc/unenc_impermanence.html\">Unencrypted Impermanence</a></p>\n</li>\n<li>\n<p>You can still add Lanzaboote and sops secrets after the install for a more\nsecure system. To get the full benefits of Lanzaboote it is recommended to\nuse full disk encryption.</p>\n</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h2>2. Encrypted Disko Btrfs Subvol Installation</h2>\n<ul>\n<li>\n<p><strong>Encrypted Install Guide:</strong>\n<a href=\"https://saylesss88.github.io/installation/enc/enc_install.html\">Encrypted Install</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/installation/enc/encrypted_impermanence.html\">Encrypted Impermanence</a></p>\n</li>\n<li>\n<p><strong>Important Considerations:</strong></p>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/installation/enc/lanzaboote.html\">Secure Boot with Lanzaboote</a>\nFor the full benefit of Secure Boot (with Lanzaboote), it’s highly\nrecommended to have a second stage of protection, such as an encrypted disk.</p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">Adding Sops</a>\nYou can easily add <code>sops</code> (for managing encrypted secrets) to your\nconfiguration <em>after</em> the initial encrypted installation and reboot. This\ncan simplify the initial setup process. However, always remember the core\ngoal of using encrypted secrets: <strong>never commit unencrypted or even hashed\nsensitive data directly into your Git repository.</strong> With modern equipment\nbrute force attacks are a real threat.</p>\n</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h2>3. Post-Installation Security &amp; Features</h2>\n<p>Once your base NixOS system is installed, consider these powerful additions:</p>\n<ul>\n<li>\n<p><strong><code>sops-nix</code>:</strong> For managing encrypted secrets directly within your NixOS\nconfiguration, ensuring sensitive data is never stored in plain text.</p>\n</li>\n<li>\n<p><strong><code>lanzaboote</code>:</strong> For enabling Secure Boot, verifying the integrity of your\nboot chain (requires UEFI and custom keys).</p>\n</li>\n<li>\n<p><strong><code>impermanence</code>:</strong> For setting up a stateless NixOS system, where the root\nfilesystem reverts to a clean state on every reboot.</p>\n</li>\n</ul>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/unenc/unencrypted_setups.html",
      "url": "https://saylesss88.github.io/installation/unenc/unencrypted_setups.html",
      "title": "Unencrypted Install",
      "content_html": "<h1>Minimal BTRFS-Subvol Install with Disko and Flakes</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<h1>Unencrypted Setups</h1>\n<p>Figure 1: BTRFS Logo: Image of the BTRFS logo. Sourced from the BTRFS repo BTRFS\nlogo</p>\n<p>Why I Chose BTRFS I chose BTRFS because I was already familiar with it from\nusing it with Arch Linux and I found it to be very easy to use. From what I’ve\nread, there are licensing issues between the Linux Kernel and ZFS which means\nthat ZFS is not part of the Linux Kernel; it’s maintained by the OpenZFS project\nand available as a separate kernel module. This can cause issues and make you\nthink more about your filesystem than I personally want to at this point.</p>\n<details>\n<summary>✔️ Click for BTRFS Subvolume Overview</summary>\n<p>A <strong>Btrfs subvolume</strong> is essentially a distinct section within a Btrfs\nfilesystem that maintains its own set of files and directories, along with a\nseparate inode numbering system. Unlike block-level partitions (such as LVM\nlogical volumes), Btrfs subvolumes operate at the file level and are based on\nfile extents.</p>\n<p><strong>Extents</strong> in Btrfs are contiguous blocks of data on disk that store the actual\ncontents of files. When files are created or modified, Btrfs manages these\nextents efficiently, allowing features like deduplication and snapshots.\nMultiple subvolumes can reference the same extents, meaning that identical data\nis not duplicated on disk, which saves space and improves performance.</p>\n<p>A <strong>snapshot</strong> in Btrfs is a special kind of subvolume that starts with the same\ncontent as another subvolume at the time the snapshot is taken. Snapshots are\ntypically writable by default, so you can make changes in the snapshot without\naffecting the original subvolume. This is possible because Btrfs tracks changes\nat the extent level, only creating new extents when files are modified (a\ntechnique called copy-on-write).</p>\n<p>Subvolumes in Btrfs behave much like regular directories from a user’s\nperspective, but they support additional operations such as renaming, moving,\nand nesting (placing subvolumes within other subvolumes). There are no\nrestrictions on nesting, though it can affect how snapshots are created and\nmanaged. Each subvolume is assigned a unique and unchangeable numeric ID\n(subvolid or rootid).</p>\n<p>You can access a Btrfs subvolume in two main ways:</p>\n<ul>\n<li>\n<p>As a normal directory within the filesystem.</p>\n</li>\n<li>\n<p>By mounting it directly as if it were a separate filesystem, using the subvol\nor subvolid mount options. When mounted this way, you only see the contents of\nthat subvolume, similar to how a bind mount works.</p>\n</li>\n</ul>\n<p>When a new Btrfs filesystem is created, it starts with a “top-level” subvolume\n(with an internal ID of 5). This subvolume is always present and cannot be\ndeleted or replaced, and it is the default mount point unless changed with btrfs\nsubvolume set-default.</p>\n<p>Subvolumes can also have storage quotas set using Btrfs’s quota groups , but\notherwise, they all draw from the same underlying storage pool. Thanks to\nfeatures like deduplication and snapshots, subvolumes can share data efficiently\nat the extent level.While ZFS is a solid choice and offers some benefits over\nBTRFS, I recommend looking into it before making your own decision.</p>\n<p>If you have a ton of RAM you could most likely skip the minimal install and just\nset your system up as needed or just use\n<a href=\"https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/\">tmpfs as root</a></p>\n</details>\n<h2>Getting Started with Disko</h2>\n<p>Disko allows you to declaratively partition and format your disks, and then\nmount them to your system. I recommend checking out the\n<a href=\"https://github.com/nix-community/disko/tree/master?tab=readme-ov-file\">README</a>\nas it is a disk destroyer if used incorrectly.</p>\n<p>We will mainly be following the\n<a href=\"https://github.com/nix-community/disko/blob/master/docs/quickstart.md\">disko quickstart guide</a></p>\n<p>Figure 2: <strong>Disko Logo</strong>: Image of the logo for Disko, the NixOS declarative\ndisk partitioning tool. Sourced from the\n<a href=\"https://github.com/nix-community/disko\">Disko project</a> disko logo</p>\n<ol>\n<li>Get the\n<a href=\"https://channels.nixos.org/nixos-25.05/latest-nixos-minimal-x86_64-linux.iso\">Nixos Minimal ISO</a>\nGet it on a usb stick, I use Ventoy with Ventoy2Disk.sh. The following is the\nlink to the\n<a href=\"https://sourceforge.net/projects/ventoy/files/v1.1.05/ventoy-1.1.05-linux.tar.gz/download\">Ventoy TarBall</a>\ndownload, untar it with <code>tar -xzf ventoy-1.1.05-linux.tar.gz</code>, and make it\nexecutable with <code>chmod +x Ventoy2Disk.sh</code>, and finally execute it with\n<code>sudo ./Ventoy2Disk.sh</code> Follow the prompts to finish the install.</li>\n</ol>\n<p>You’ll have to run it on for the USB drive you’re trying to use, you can do that\nby unplugging the USB stick and running <code>lsblk</code>, then plug it in again and run:</p>\n<pre><code class=\"language-bash\">lsblk -f\nNAME          FSTYPE      FSVER LABEL   UUID                                 FSAVAIL FSUSE% MOUNTPOINTS\nsda\n└─sda1        vfat        FAT32 MYUSB   46E8-9304\nsdb           vfat        FAT12         F054-697D                               1.4M     0% /run/media/jr/F054-697D\nnvme0n1\n├─nvme0n1p1   vfat        FAT32         BCD8-8C51                               1.8G    12% /boot\n</code></pre>\n<ul>\n<li><code>sdb</code> is a USB plugin for a mouse. <code>sda</code> is the USB stick that I want to\ntarget here:</li>\n</ul>\n<pre><code class=\"language-bash\">sudo ./Ventoy2Disk.sh -i /dev/sda\n# Or to force overwrite an existing Ventoy entry\nsudo ./Ventoy2Disk.sh -I /dev/sda\n</code></pre>\n<ol start=\"2\">\n<li>The minimal installer uses wpa_supplicant instead of NetworkManager, to\nenable networking run the following:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo systemctl start wpa_supplicant\nwpa_cli\n</code></pre>\n<pre><code class=\"language-bash\">&gt; add_network\n0\n\n&gt; set_network 0 ssid \"myhomenetwork\"\nOK\n\n&gt; set_network 0 psk \"mypassword\"\nOK\n\n&gt; enable_network 0\nOK\n</code></pre>\n<p>To exit type <code>quit</code>, then check your connection with <code>ping google.com</code>.</p>\n<p>Another option is to do the following, so either the above method or the below\nmethod after starting <code>wpa_supplicant</code>:</p>\n<pre><code class=\"language-bash\"># Alternative for quick setup (less interactive, but often faster)\nsudo wpa_passphrase \"myhomenetwork\" \"mypassword\" &gt;&gt; /etc/wpa_supplicant/wpa_supplicant-wlan0.conf\nsudo systemctl restart wpa_supplicant@wlan0.service\n</code></pre>\n<ol start=\"3\">\n<li>Get your Disk Name with lsblk</li>\n</ol>\n<p>The output should be something like:</p>\n<pre><code class=\"language-bash\">NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS\nnvme0n1     259:0    0   1,8T  0 disk\n</code></pre>\n<ol start=\"4\">\n<li>Copy the disk configuration to your machine. You can choose one from the\nexamples directory.</li>\n</ol>\n<ul>\n<li><strong>Option A</strong>: (Simpler for new users) I also created a starter repo containing\nmuch of what’s needed. If you choose this option follow the README.md included\nwith the repo.</li>\n</ul>\n<pre><code class=\"language-bash\">cd ~\ngit clone https://github.com/saylesss88/my-flake.git\n</code></pre>\n<blockquote>\n<p>Make sure to change line 7 in disk-config.nix to what you got from step 3\ndevice = “/dev/nvme0n1”;</p>\n</blockquote>\n<ul>\n<li><strong>Option B</strong>: (More flexible, more manual steps) Skip cloning the repo above\nand for the btrfs-subvolume default layout, run the following:</li>\n</ul>\n<pre><code class=\"language-bash\">cd /tmp\ncurl https://raw.githubusercontent.com/nix-community/disko/refs/heads/master/example/btrfs-subvolumes.nix -o /tmp/disk-config.nix\n</code></pre>\n<ol start=\"5\">\n<li>Make Necessary changes, I set mine up for impermanence with the following:</li>\n</ol>\n<pre><code class=\"language-bash\">nano /tmp/disk-config.nix\n</code></pre>\n<pre><code class=\"language-nix\">{\n  disko.devices = {\n    disk = {\n      main = {\n        type = \"disk\";\n        device = \"/dev/nvme0n1\";\n        content = {\n          type = \"gpt\";\n          partitions = {\n            ESP = {\n              priority = 1;\n              name = \"ESP\";\n              start = \"1M\";\n              end = \"512M\";\n              type = \"EF00\";\n              content = {\n                type = \"filesystem\";\n                format = \"vfat\";\n                mountpoint = \"/boot\";\n                mountOptions = [\"umask=0077\"];\n              };\n            };\n            root = {\n              size = \"100%\";\n              content = {\n                type = \"btrfs\";\n                extraArgs = [\"-f\"]; # Override existing partition\n                # Subvolumes must set a mountpoint in order to be mounted,\n                # unless their parent is mounted\n                subvolumes = {\n                  # Subvolume name is different from mountpoint\n                  \"/root\" = {\n                    mountpoint = \"/\";\n                    mountOptions = [\"subvol=root\" \"compress=zstd\" \"noatime\"];\n                  };\n                  # Subvolume name is the same as the mountpoint\n                  \"/home\" = {\n                    mountOptions = [\"subvol=home\" \"compress=zstd\" \"noatime\"];\n                    mountpoint = \"/home\";\n                  };\n                  # Sub(sub)volume doesn't need a mountpoint as its parent is mounted\n                  \"/home/user\" = {};\n                  # Parent is not mounted so the mountpoint must be set\n                  \"/nix\" = {\n                    mountOptions = [\n                      \"subvol=nix\"\n                      \"compress=zstd\"\n                      \"noatime\"\n                    ];\n                    mountpoint = \"/nix\";\n                  };\n                  \"/nix/persist\" = {\n                    mountpoint = \"/nix/persist\";\n                    mountOptions = [\"subvol=persist\" \"compress=zstd\" \"noatime\"];\n                  };\n                  \"/log\" = {\n                    mountpoint = \"/var/log\";\n                    mountOptions = [\"subvol=log\" \"compress=zstd\" \"noatime\"];\n                  };\n                  \"/lib\" = {\n                    mountpoint = \"/var/lib\";\n                    mountOptions = [\"subvol=lib\" \"compress=zstd\" \"noatime\"];\n                  };\n                  # This subvolume will be created but not mounted\n                  \"/test\" = {};\n                };\n              };\n            };\n          };\n        };\n      };\n    };\n  };\n  fileSystems.\"/nix/persist\".neededForBoot = true;\n  fileSystems.\"/var/log\".neededForBoot = true;\n  fileSystems.\"/var/lib\".neededForBoot = true;\n}\n</code></pre>\n<ul>\n<li>For <code>/tmp</code> on RAM use something like the following. I’ve found that having\ndisko manage swaps causes unnecessary issues. Using zram follows the ephemeral\nroute:</li>\n</ul>\n<pre><code class=\"language-nix\">{\n  lib,\n  config,\n  ...\n}: let\n  cfg = config.custom.zram;\nin {\n  options.custom.zram = {\n    enable = lib.mkEnableOption \"Enable utils module\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    zramSwap = {\n      enable = true;\n      # one of \"lzo\", \"lz4\", \"zstd\"\n      algorithm = \"zstd\";\n       priority = 5;\n       memoryPercent = 50;\n    };\n  };\n}\n</code></pre>\n<p>And in your <code>configuration.nix</code> you would add:</p>\n<pre><code class=\"language-nix\"># configuration.nix\ncustom = {\n    zram.enable = true;\n};\n</code></pre>\n<p>After adding the above module, you can see it with:</p>\n<pre><code class=\"language-bash\">swapon --show\nNAME       TYPE      SIZE USED PRIO\n/dev/zram0 partition 7.5G   0B    5\n</code></pre>\n<ol start=\"6\">\n<li>Run disko to partition, format and mount your disks. Warning this will wipe\nEVERYTHING on your disk. Disko doesn’t work with dual boot.</li>\n</ol>\n<pre><code class=\"language-bash\">sudo nix --experimental-features \"nix-command flakes\" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix\n</code></pre>\n<p>Check it with the following:</p>\n<pre><code class=\"language-bash\">mount | grep /mnt\n</code></pre>\n<p>The output for an nvme0n1 disk would be similar to the following:</p>\n<pre><code class=\"language-bash\">#... snip ...\n/dev/nvme0n1p2 on /mnt type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=285,subvol=/root)\n/dev/nvme0n1p2 on /mnt/persist type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=261,subvol=/persist)\n/dev/nvme0n1p2 on /mnt/etc type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=261,subvol=/persist)\n/dev/nvme0n1p2 on /mnt/nix type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=260,subvol=/nix)\n/dev/nvme0n1p2 on /mnt/var/lib type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=258,subvol=/lib)\n/dev/nvme0n1p2 on /mnt/var/log type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=259,subvol=/log)\n/dev/nvme0n1p2 on /mnt/nix/store type btrfs (ro,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=260,subvol=/nix)\n# ... snip ...\n</code></pre>\n<ol start=\"7\">\n<li>Generate necessary files, here we use –no-filesystems because disko handles\nthe fileSystems attribute for us.</li>\n</ol>\n<pre><code class=\"language-bash\">nixos-generate-config --no-filesystems --root /mnt\n</code></pre>\n<p>It may be helpful to add a couple things to your <code>configuration.nix</code> now,\nrebuild and then move on. Such as, your hostname, git, an editor of your choice.\nAfter your additions run <code>sudo nixos-rebuild</code> switch to apply the changes. If\nyou do this, you can skip the <code>nix-shell -p</code> command coming up.</p>\n<pre><code class=\"language-bash\">sudo mv /tmp/disk-config.nix /mnt/etc/nixos\n</code></pre>\n<h2>Setting a Flake for your minimal Install</h2>\n<ol start=\"8\">\n<li>Create the flake in your home directory, then move it to /mnt/etc/nixos. This\navoids needing to use sudo for every command while in the /mnt/etc/nixos\ndirectory.</li>\n</ol>\n<pre><code class=\"language-bash\">cd ~\nmkdir flake &amp;&amp; cd flake\nnix-shell -p git yazi helix\nexport NIX_CONFIG='experimental-features = nix-command flakes'\nexport EDITOR='hx'\nhx flake.nix\n</code></pre>\n<blockquote>\n<p>You’ll change hostname = nixpkgs.lib.nixosSystem to your chosen hostname,\n(e.g. magic = nixpkgs.lib.nixosSystem). This will be the same as your\nnetworking.hostName = “magic”; in your configuration.nix that we will set up\nshortly.</p>\n</blockquote>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    disko.url = \"github:nix-community/disko/latest\";\n    disko.inputs.nixpkgs.follows = \"nixpkgs\";\n    # impermanence.url = \"github:nix-community/impermanence\";\n  };\n\n  outputs = inputs@{ nixpkgs, ... }: {\n    nixosConfigurations = {\n      # Change `my-hostname` to match `networking.hostName`\n      my-hostname = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          inputs.disko.nixosModules.disko\n          # inputs.impermanence.nixosModules.impermanence\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<p>Move all the files into your flake:</p>\n<pre><code class=\"language-bash\">cd /mnt/etc/nixos/\nsudo mv disk-config.nix hardware-configuration.nix configuration.nix ~/flake\n</code></pre>\n<ol start=\"9\">\n<li>Edit configuration.nix with what is required, the following is required, I\nclone my original flake repo and move the pieces into place but it’s fairly\neasy to just type it all out:</li>\n</ol>\n<ul>\n<li>\n<p>Bootloader, (e.g., boot.loader.systemd-boot.enable = true;)</p>\n</li>\n<li>\n<p>User, the example uses username change this to your chosen username. If you\ndon’t set your hostname it will be nixos.</p>\n</li>\n<li>\n<p>Networking, networking.networkmanager.enable = true;</p>\n</li>\n<li>\n<p><code>hardware-configuration.nix</code> &amp; <code>disk-config.nix</code> for this setup</p>\n</li>\n<li>\n<p><code>initialHashedPassword</code>: Run <code>mkpasswd --method=yescrypt</code>, then enter your\ndesired password. Example output,</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">mkpasswd --method=yescrypt &gt; /tmp/pass.txt\n</code></pre>\n<ul>\n<li>You can check the quality with pwscore:</li>\n</ul>\n<pre><code class=\"language-bash\">nix-shell -p libpwquality\n\npwscore\nvery-secure-password\n100\n</code></pre>\n<p>read the hashed password into the file with :r /tmp/pass.txt and move it into\nplace.</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{\n  config,\n  lib,\n  pkgs,\n  inputs,\n  ...\n}: {\n  imports = [\n    # Include the results of the hardware scan.\n    ./hardware-configuration.nix\n    ./disk-config.nix\n  ];\n\n  networking.hostName = \"my-hostname\"; # This will match the `hostname` of your flake\n\n  networking.networkmanager.enable = true;\n\n  boot.loader.systemd-boot.enable = true; # (for UEFI systems only)\n  # List packages installed in system profile.\n  # You can use https://search.nixos.org/ to find more packages (and options).\n  environment.systemPackages = with pkgs; [\n    vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.\n    #   wget\n    git\n  ];\n\n  time.timeZone = \"America/New_York\";\n\n# Change `nixos` to your chosen username, change the group to match\n  users.users.nixos = {\n    isNormalUser = true;\n    extraGroups = [ \"wheel\" \"networkmanager\" ]; # Add \"wheel\" for sudo access\n    initialHashedPassword = \"COPY_YOUR_MKPASSWD_OUTPUT_HERE\"; # &lt;-- This is where it goes!\n    # home = \"/home/nixos\"; # Optional: Disko typically handles home subvolumes\n  };\n  # Create a matching group\n  users.groups.nixos = {};\n\n  console.keyMap = \"us\";\n\n  nixpkgs.config.allowUnfree = true;\n\n  system.stateVersion = \"25.05\";\n}\n</code></pre>\n<p>Shred pass.txt:</p>\n<pre><code class=\"language-bash\">shred /tmp/pass.txt\nrm /tmp/pass.txt\n</code></pre>\n<ol start=\"10\">\n<li>Move the flake to /mnt/etc/nixos and run nixos-install:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mv ~/flake /mnt/etc/nixos/\nsudo nixos-install --flake /mnt/etc/nixos/flake .#hostname\n# if the above command doesn't work try this:\nsudo nixos-install --flake /mnt/etc/nixos/flake#hostname\n</code></pre>\n<p>You will be prompted to enter a new password if everything succeeds.</p>\n<p>If everything checks out, reboot the system and you should be prompted to enter\nyour user and password to login to a shell to get started.</p>\n<p>The flake will be placed at <code>/etc/nixos/flake</code>, I choose to move it to my home\ndirectory. Since the file was first in <code>/etc</code> you’ll need to adjust the\npermissions with something like <code>sudo chown nixos:nixos ~/flake</code>. This is based\noff of the example above where we created both a nixos user and group.</p>\n<p>You can check the layout of your btrfs system with:</p>\n<pre><code class=\"language-bash\">sudo btrfs subvolume list /\n</code></pre>\n<ul>\n<li>You may notice some old_roots in the output, which are snapshots, which are\nlikely created before system upgrades or reboots for rollback purposes. They\ncan be deleted or rolled back as needed.</li>\n</ul>\n<p><a href=\"https://btrfs.readthedocs.io/en/latest/Subvolumes.html\">BTRFS Subvolumes</a></p>\n<p>To continue following along and set up impermanence\n<a href=\"https://saylesss88.github.io/installation/unencrypted/impermanence.html\">Click Here</a></p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/enc_install.html",
      "url": "https://saylesss88.github.io/installation/enc/enc_install.html",
      "title": "Encrypted Install (BTRFS)",
      "content_html": "<h1>Encrypted Setups</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>NixOS supports file systems that are encrypted using LUKS (Linux Unified Key\nSetup). This guide walks you through an encrypted NixOS installation using Disko\nfor disk management and Btrfs for subvolumes. It is designed for users who want\nfull disk encryption and a modern filesystem layout. If you prefer an\nunencrypted setup, you can skip the LUKS and encryption steps, but this guide\nfocuses on security and flexibility.</p>\n<ul>\n<li>For Unencrypted layout\n<a href=\"https://saylesss88.github.io/installation/unencrypted/unencrypted.html\">Click Here</a></li>\n</ul>\n<p>If you choose to set up impermanence, ensure it matches your install. Encrypted\nSetup with Encrypted Impermanence and Unencrypted Setup with Unencrypted\nImpermanence.</p>\n<blockquote>\n<p>❗ NOTE: This is a bit convoluted, there are a few paths you can follow. If\nyou choose to use the starter repo (<a href=\"https://github.com/saylesss88/my-flake\">https://github.com/saylesss88/my-flake</a>)\njust follow the included README and use this for reference.</p>\n</blockquote>\n<h2>What does LUKS Encryption Protect?</h2>\n<p>It’s important to understand what disk encryption protects and what it doesn’t\nprotect so you don’t have any misconceptions about how safe your data is.</p>\n<ul>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Full_Disk_Encryption\">NixOS Wiki FDE</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.archlinux.org/title/Data-at-rest_encryption\">Arch Wiki Data-at-rest encryption</a></p>\n</li>\n<li>\n<p><a href=\"https://0pointer.net/blog/authenticated-boot-and-disk-encryption-on-linux.html\">Authenticated Booot and DE on Linux</a></p>\n</li>\n<li>\n<p><a href=\"https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/\">Bypassing FDE with TPM2 Unlock</a></p>\n</li>\n</ul>\n<p><strong>What LUKS Protects</strong>:</p>\n<ul>\n<li>\n<p><strong>Data Confidentiality at Rest</strong>: LUKS encrypts entire block devices (such as\ndisk partitions or whole drives), ensuring that all data stored on the\nencrypted device is unreadable without the correct decryption key or\npassphrase. This protects sensitive information from unauthorized access if\nthe device is lost, stolen, or physically accessed by an attacker.</p>\n</li>\n<li>\n<p><strong>Physical Security</strong>: If someone gains physical possession of your device\n(for example, by stealing your laptop or removing a hard drive), LUKS ensures\nthe data remains inaccessible and appears as random, meaningless bytes without\nthe correct credentials.</p>\n</li>\n<li>\n<p><strong>Protection Against Offline Attacks</strong>: LUKS defends against attackers who\nattempt to bypass the operating system by booting from another device or\nremoving the drive and mounting it elsewhere. Without the decryption key, the\ndata remains protected.</p>\n</li>\n</ul>\n<p><strong>What LUKS Does Not Protect</strong>:</p>\n<ul>\n<li>\n<p><strong>Data in Use</strong>: Once the system is booted and the encrypted device is\nunlocked, the data becomes accessible to the operating system and any user or\nprocess with the necessary permissions. LUKS does not protect against attacks\non a running system, such as malware, remote exploits, or unauthorized users\nwith access to an unlocked session.</p>\n</li>\n<li>\n<p><strong>File-Level Access Control</strong>: LUKS encrypts entire partitions or disks, not\nindividual files or directories. It does not provide granular file-level\nencryption or access control within the operating system.</p>\n</li>\n<li>\n<p><strong>Network Attacks</strong>: LUKS only protects data stored on disk. It does not\nencrypt data transmitted over networks or protect against network-based\nattacks.</p>\n</li>\n<li>\n<p><strong>Bootloader and EFI Partitions</strong>: The initial bootloader or EFI system\npartition cannot be encrypted with LUKS, so some parts of the boot process may\nremain exposed unless additional measures are taken. (i.e., Secure Boot,\nadditional passwords, TPM2)</p>\n</li>\n</ul>\n<p>To Sum it Up: LUKS encryption protects the confidentiality of all data stored on\nan encrypted block device by making it unreadable without the correct passphrase\nor key. This ensures that, if your device is lost or stolen, your data remains\nsecure and inaccessible to unauthorized users. However, LUKS does not protect\ndata once the system is unlocked and running, nor does it provide file-level\nencryption or protect against malware and network attacks. For comprehensive\nsecurity, LUKS should be combined with strong access controls and other security\nbest practices.</p>\n<h2>The Install</h2>\n<ol>\n<li>\n<p>Get the\n<a href=\"https://channels.nixos.org/nixos-25.05/latest-nixos-minimal-x86_64-linux.iso\">Nixos Minimal ISO</a>\nGet it on a usb stick, I use Ventoy with Ventoy2Disk.sh. The following is the\nlink to the\n<a href=\"https://sourceforge.net/projects/ventoy/files/v1.1.05/ventoy-1.1.05-linux.tar.gz/download\">Ventoy TarBall</a>\ndownload, untar it with <code>tar -xzf ventoy-1.1.05-linux.tar.gz</code>, and make it\nexecutable with <code>chmod +x Ventoy2Disk.sh</code>, and finally execute it with\n<code>sudo bash Ventoy2Disk.sh</code> Follow the prompts to finish the install.</p>\n</li>\n<li>\n<p>Configuring Networking</p>\n</li>\n</ol>\n<p>The minimal installer uses <code>wpa_supplicant</code> instead of NetworkManager. Choose\none of the following methods to enable networking:</p>\n<pre><code class=\"language-bash\">sudo systemctl start wpa_supplicant\nwpa_cli\n</code></pre>\n<h3>Option A: Interactive <code>wpa_cli</code></h3>\n<pre><code class=\"language-bash\">&gt; add_network\n0\n\n&gt; set_network 0 ssid \"myhomenetwork\"\nOK\n\n&gt; set_network 0 psk \"mypassword\"\nOK\n\n&gt; enable_network 0\nOK\n</code></pre>\n<p>To exit type <code>quit</code>, then check your connection with <code>ping google.com</code>.</p>\n<h3>Option B: Non-Interactive <code>wpa_passphrase</code></h3>\n<p>This method is quicker for known networks and persists the configuration for the\nlive environment.</p>\n<p>First, identify your wireless interface name (e.g., <code>wlan0</code>) using <code>ip a</code>.</p>\n<pre><code class=\"language-bash\">sudo systemctl start wpa_supplicant # Ensure wpa_supplicant is running\n# This command generates the config and appends it to a file specific to wlan0\nsudo wpa_passphrase \"myhomenetwork\" \"mypassword\" | sudo tee /etc/wpa_supplicant/wpa_supplicant-wlan0.conf\nsudo systemctl restart wpa_supplicant@wlan0.service\n</code></pre>\n<p>After either method, exit <code>wpa_cli</code> with <code>quit</code>. Then test your connection:</p>\n<pre><code class=\"language-bash\">ping 1.1.1.1\n</code></pre>\n<ol start=\"3\">\n<li>Get your Disk Name with <code>lsblk</code></li>\n</ol>\n<p>The output should be something like:</p>\n<pre><code class=\"language-bash\">NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS\nnvme0n1     259:0    0   1,8T  0 disk\n</code></pre>\n<blockquote>\n<p>❗ From here, you can either</p>\n</blockquote>\n<ol start=\"4\">\n<li>Copy the disk configuration to your machine. You can choose one from the\n<a href=\"https://github.com/nix-community/disko/tree/master/example\">examples directory</a>.</li>\n</ol>\n<p>There is still a starter repo that can save you some typing, make sure to\ncarefully review if you decide to use it:</p>\n<pre><code class=\"language-bash\">export NIX_CONFIG='experimental-features = nix-command flakes'\nexport EDITOR='hx' # or 'vi'\nnix-shell -p git yazi helix mkpasswd\ngit config --global user.name \"gitUsername\"\ngit config --global user.email \"gitEmail\"\n# OPTIONAL starter repo containing disk-config set up for impermanence\ngit clone https://github.com/saylesss88/my-flake.git\n</code></pre>\n<p>I prefer <code>helix</code> here as it’s defaults are great. (i.e., auto closing brackets\nand much more)</p>\n<p>If you choose to use the starter repo you won’t need to run the next command as\nit is already populated in the repo and should use the\n<a href=\"https://github.com/saylesss88/my-flake\">Starter Repo README</a> most of the rest\nof the guide is for manual disko without the starter repo.</p>\n<p>If you click on the layout you want then click the <code>Raw</code> button near the top,\nthen copy the <code>url</code> and use it in the following command:</p>\n<pre><code class=\"language-bash\">cd /tmp\ncurl https://raw.githubusercontent.com/nix-community/disko/refs/heads/master/example/luks-btrfs-subvolumes.nix -o /tmp/disk-config.nix\n</code></pre>\n<p>The above curl command is to the <code>luks-btrfs-subvolumes.nix</code> layout.</p>\n<ol start=\"5\">\n<li>Make Necessary changes, I prepared mine for impermanence with the following:</li>\n</ol>\n<pre><code class=\"language-bash\">hx /tmp/disk-config.nix\n</code></pre>\n<p>Make sure you identify your system disk name with <code>lsblk</code> and change the\n<code>device</code> attribute below to match your disk.</p>\n<pre><code class=\"language-bash\">lsblk\nnvme0n1       259:0    0 476.9G  0 disk\n├─nvme0n1p1   259:1    0   512M  0 part  /boot\n└─nvme0n1p2   259:2    0 476.4G  0 part\n</code></pre>\n<p>My disk is <code>nvme0n1</code>, change below to match yours:</p>\n<pre><code class=\"language-nix\">{\n  disko.devices = {\n    disk = {\n      nvme0n1 = {\n        type = \"disk\";\n        # Make sure this is correct with `lsblk`\n        device = \"/dev/nvme0n1\";\n        content = {\n          type = \"gpt\";\n          partitions = {\n            ESP = {\n              label = \"boot\";\n              name = \"ESP\";\n              size = \"1G\";\n              type = \"EF00\";\n              content = {\n                type = \"filesystem\";\n                format = \"vfat\";\n                mountpoint = \"/boot\";\n                mountOptions = [\n                  \"defaults\"\n                ];\n              };\n            };\n            luks = {\n              size = \"100%\";\n              label = \"luks\";\n              content = {\n                type = \"luks\";\n                name = \"cryptroot\";\n                content = {\n                  type = \"btrfs\";\n                  extraArgs = [\"-L\" \"nixos\" \"-f\"];\n                  subvolumes = {\n                    \"/root\" = {\n                      mountpoint = \"/\";\n                      mountOptions = [\"subvol=root\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/root-blank\" = {\n                      mountOptions = [\"subvol=root-blank\" \"nodatacow\" \"noatime\"];\n                    };\n                    \"/home\" = {\n                      mountpoint = \"/home\";\n                      mountOptions = [\"subvol=home\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/nix\" = {\n                      mountpoint = \"/nix\";\n                      mountOptions = [\"subvol=nix\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/persist\" = {\n                      mountpoint = \"/persist\";\n                      mountOptions = [\"subvol=persist\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/log\" = {\n                      mountpoint = \"/var/log\";\n                      mountOptions = [\"subvol=log\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/lib\" = {\n                      mountpoint = \"/var/lib\";\n                      mountOptions = [\"subvol=lib\" \"compress=zstd\" \"noatime\"];\n                    };\n                    \"/persist/swap\" = {\n                      mountpoint = \"/persist/swap\";\n                      mountOptions = [\"subvol=swap\" \"noatime\" \"nodatacow\" \"compress=no\"];\n                      swap.swapfile.size = \"18G\";\n                    };\n                  };\n                };\n              };\n            };\n          };\n        };\n      };\n    };\n  };\n\n  fileSystems.\"/persist\".neededForBoot = true;\n  fileSystems.\"/var/log\".neededForBoot = true;\n  fileSystems.\"/var/lib\".neededForBoot = true;\n}\n</code></pre>\n<p>I have 16G of RAM so to be safe for hibernation I chose to give it some extra\nspace. The boot partition is 1G, this extra space is for specialisations and\nlanzaboote.</p>\n<p>or for a swapfile:</p>\n<pre><code class=\"language-nix\">swapDevices = [\n  {\n    device = \"/persist/swap/swapfile\";\n    size = 18 * 1024; # Size in MB (18GB)\n    # or\n    # size = 16384; # Size in MB (16G);\n  }\n];\n</code></pre>\n<h2>Setting up zram and /tmp on RAM</h2>\n<p>While <code>/tmp</code> is handled by <code>tmpfs</code> (as shown the below <code>configuration.nix</code>), you\ncan further enhance memory efficiency with <code>zram</code> for compressed swap, as shown\nbelow.</p>\n<blockquote>\n<pre><code class=\"language-nix\">{\n  lib,\n  config,\n  ...\n}: let\n  cfg = config.custom.zram;\nin {\n  options.custom.zram = {\n    enable = lib.mkEnableOption \"Enable utils module\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    zramSwap = {\n      enable = true;\n      # one of \"lzo\", \"lz4\", \"zstd\"\n      algorithm = \"zstd\";\n       priority = 5;\n       memoryPercent = 50;\n    };\n  };\n}\n</code></pre>\n<p>And in your <code>configuration.nix</code> you would add:</p>\n<pre><code class=\"language-nix\"># configuration.nix\ncustom = {\n    zram.enable = true;\n};\n</code></pre>\n</blockquote>\n<p>After adding the above module and rebuilding, you can see it with:</p>\n<pre><code class=\"language-bash\">swapon --show\nNAME       TYPE      SIZE USED PRIO\n/dev/zram0 partition 7.5G   0B    5\n</code></pre>\n<ol start=\"6\">\n<li>Run disko to partition, format and mount your disks. <strong>Warning</strong> this will\nwipe <strong>EVERYTHING</strong> on your disk. Disko doesn’t work with dual boot.</li>\n</ol>\n<pre><code class=\"language-bash\">sudo nix --experimental-features \"nix-command flakes\" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix\n</code></pre>\n<p>Check it with the following:</p>\n<pre><code class=\"language-bash\">mount | grep /mnt\n</code></pre>\n<p>The output for an <code>nvme0n1</code> disk would be similar to the following:</p>\n<pre><code class=\"language-bash\">#... snip ...\n/dev/nvme0n1p2 on /mnt type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=285,subvol=/root)\n/dev/nvme0n1p2 on /mnt/persist type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=261,subvol=/persist)\n/dev/nvme0n1p2 on /mnt/etc type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=261,subvol=/persist)\n/dev/nvme0n1p2 on /mnt/nix type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=260,subvol=/nix)\n/dev/nvme0n1p2 on /mnt/var/lib type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=258,subvol=/lib)\n/dev/nvme0n1p2 on /mnt/var/log type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=259,subvol=/log)\n/dev/nvme0n1p2 on /mnt/nix/store type btrfs (ro,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvolid=260,subvol=/nix)\n# ... snip ...\n</code></pre>\n<ol start=\"7\">\n<li>Generate necessary files, here we use <code>--no-filesystems</code> because disko\nhandles the <code>fileSystems</code> attribute for us.</li>\n</ol>\n<pre><code class=\"language-bash\">nixos-generate-config --no-filesystems --root /mnt\n</code></pre>\n<ul>\n<li>The above command will place a <code>configuration.nix</code> and\n<code>hardware-configuration.nix</code> in <code>/mnt/etc/nixos/</code></li>\n</ul>\n<p>It may be helpful to add a couple things to your <code>configuration.nix</code> now, while\nit’s in its default location. You can just add what you want and rebuild once\nwith <code>sudo nixos-rebuild switch</code> and move on. (i.e. <code>git</code>, an editor, etc.).</p>\n<h3>Setting a Flake for your minimal Install</h3>\n<ol start=\"8\">\n<li>Create the flake in your home directory to avoid needing to use sudo for\nevery command:</li>\n</ol>\n<pre><code class=\"language-bash\">cd   # Move to home directory\nmkdir flake\ncd /mnt/etc/nixos/\nsudo mv hardware-configuration.nix configuration.nix ~/flake/\nsudo mv /tmp/disk-config.nix ~/flake/\n</code></pre>\n<pre><code class=\"language-bash\">cd flake\nhx flake.nix\n</code></pre>\n<blockquote>\n<p>You’ll change <code>hostName = nixpkgs.lib.nixosSystem</code> to your chosen hostname,\n(e.g. <code>magic = nixpkgs.lib.nixosSystem</code>). This will be the same as your\n<code>networking.hostName = \"magic\";</code> in your <code>configuration.nix</code> that we will set\nup shortly.</p>\n</blockquote>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    disko.url = \"github:nix-community/disko/latest\";\n    disko.inputs.nixpkgs.follows = \"nixpkgs\";\n    # impermanence.url = \"github:nix-community/impermanence\";\n  };\n\n  outputs = inputs@{ nixpkgs, ... }: {\n    nixosConfigurations = {\n      # Change `hostName` to your chosen host name\n      nixos = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          inputs.disko.nixosModules.disko\n          # inputs.impermanence.nixosModules.impermanence\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<ol start=\"9\">\n<li>Edit <code>configuration.nix</code> with what is required, the following are required, I\nclone my original flake repo and move the pieces into place but it’s fairly\neasy to just type it all out:</li>\n</ol>\n<ul>\n<li>\n<p>Bootloader, (e.g., <code>boot.loader.systemd-boot.enable = true;</code>)</p>\n</li>\n<li>\n<p>User, the example uses <code>username</code> change this to your chosen username. If you\ndon’t set your hostname it will be <code>nixos</code>.</p>\n</li>\n<li>\n<p>Networking, <code>networking.networkmanager.enable = true;</code></p>\n</li>\n<li>\n<p><code>hardware-configuration.nix</code> &amp; <code>disk-config.nix</code> for this setup</p>\n</li>\n<li>\n<p>If you type this out by hand and mess up a single character, you will have to\nstart over completely. A fairly safe way to do this is with <code>vim</code> or <code>hx</code> and\nredirect the hashed pass to a <code>/tmp/pass.txt</code>, you can then read it into your\n<code>users.nix</code>:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">mkpasswd --method=yescrypt &gt; /tmp/pass.txt\n# Enter your chosen password\n</code></pre>\n<p>And then when inside <code>configuration.nix</code>, move to the line where you want the\nhashed password and type <code>:r /tmp/pass.txt</code> to read the hash into your current\nfile.</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{\n  config,\n  lib,\n  pkgs,\n  inputs,\n  ...\n}: {\n  imports = [\n    # Include the results of the hardware scan.\n    ./hardware-configuration.nix\n    ./disk-config.nix\n  ];\n\n  # systemd Stage 1: if enabled, it handles unlocking of LUKS-encrypted volumes during boot.\n    boot.initrd.luks.devices = {\n    cryptroot = {\n      device = \"/dev/disk/by-partlabel/luks\";\n      allowDiscards = true;\n    };\n  };\n\n  # This complements using zram, putting /tmp on RAM\n    boot = {\n    tmp = {\n      useTmpfs = true;\n      tmpfsSize = \"50%\";\n    };\n  };\n\n  # Enable autoScrub for btrfs\n    services.btrfs.autoScrub = {\n    enable = true;\n    interval = \"weekly\";\n    fileSystems = [\"/\"];\n  };\n\n\n  # Change me!\n  networking.hostName = \"nixos\"; # This will match the `hostname` of your flake\n\n  networking.networkmanager.enable = true;\n\n  boot.loader.systemd-boot.enable = true; # (for UEFI systems only)\n  # List packages installed in system profile.\n  # You can use https://search.nixos.org/ to find more packages (and options).\n  environment.systemPackages = with pkgs; [\n    vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.\n    #   wget\n    git\n  ];\n\n  time.timeZone = \"America/New_York\";\n\n# Change me to your chosen username (i.e. change nixosUser to your username)\n  users.users.nixosUser = {\n    isNormalUser = true;\n    extraGroups = [ \"wheel\" \"networkmanager\" ]; # Add \"wheel\" for sudo access\n    initialHashedPassword = \"READ_MKPASSWD_OUTPUT_HERE\"; # &lt;-- This is where it goes!\n    # home = \"/home/nixos\"; # Optional: Disko typically handles home subvolumes\n  };\n  # Change me to match your chosen username\n  users.group.nixosUser = {};\n\n  console.keyMap = \"us\";\n\n  nixpkgs.config.allowUnfree = true;\n\n  system.stateVersion = \"25.05\";\n}\n</code></pre>\n<p>Although, just adding the <code>disk-config.nix</code> works for prompting you for your\nencryption passphrase adding the following is a more robust way of ensuring Nix\nis aware of this:</p>\n<pre><code class=\"language-nix\">    boot.initrd.luks.devices = {\n    cryptroot = {\n      device = \"/dev/disk/by-partlabel/luks\";\n      allowDiscards = true;\n    };\n  };\n</code></pre>\n<ol start=\"10\">\n<li>Move the flake to <code>/mnt/etc/nixos</code> and run <code>nixos-install</code>:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mv ~/flake /mnt/etc/nixos/\n</code></pre>\n<ul>\n<li>Give everything a quick once over, insuring your host is set in both your\n<code>flake.nix</code>, and <code>configuration.nix</code>. Ensure you changed the username in the\n<code>configuration.nix</code> from <code>nixos</code> to your chosen name, this is the name you’ll\nuse to login after you enter your encryption passphrase.</li>\n</ul>\n<p>The below command uses <code>#nixos</code> because that’s what the defaults are, you’ll\nchange it to your chosen hostname.</p>\n<pre><code class=\"language-bash\">sudo nixos-install --flake /mnt/etc/nixos/flake#nixos\n</code></pre>\n<ul>\n<li>You will be prompted to enter a new password if everything succeeds.</li>\n</ul>\n<h2>Create a Blank Snapshot of /root</h2>\n<p>This is essential if you plan on using impermanence with this encrypted setup.\nWe take a snapshot of <code>/root</code> while it’s a clean slate, right after we run disko\nto format the disk.</p>\n<p>To access all of the subvolumes, we have to mount the Btrfs partitions\ntop-level.</p>\n<ol>\n<li>Unlock the LUKS device, if not already unlocked as it should be from running\ndisko:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo cryptsetup open /dev/disk/by-partlabel/luks cryptroot\n</code></pre>\n<ol start=\"2\">\n<li>Mount the Btrfs top-level (<code>subvolid=5</code>):</li>\n</ol>\n<pre><code class=\"language-bash\">sudo mount -o subvolid=5 /dev/mapper/cryptroot /mnt\n</code></pre>\n<ol start=\"3\">\n<li>List the contents:</li>\n</ol>\n<pre><code class=\"language-bash\">ls /mnt\n# you should see something like\nroot   home  nix  persist  log  lib  ...\n</code></pre>\n<ol start=\"4\">\n<li>Now we can take a snapshot of the <code>root</code> subvolume:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo btrfs subvolume snapshot -r /mnt/root /mnt/root-blank\n</code></pre>\n<ol start=\"5\">\n<li>Verify Your Blank Snapshot:</li>\n</ol>\n<p>Before continuing, make sure your blank snapshot exists. This is crucial for\nimpermanence to work properly.</p>\n<pre><code class=\"language-bash\">sudo btrfs subvolume list /mnt\n</code></pre>\n<p>You should see output containing both <code>root</code> and <code>root-blank</code> subvolumes:</p>\n<pre><code class=\"language-bash\">ID 256 gen ... path root\nID 257 gen ... path root-blank\n</code></pre>\n<p>Check that the snapshot is read only, this ensures that our snapshot will remain\nthe same as the day we took it. It was set <code>ro</code> in disko but lets check anyways:</p>\n<pre><code class=\"language-bash\">sudo btrfs property get -ts /mnt/root-blank\n# output should be\nro=true\n</code></pre>\n<ol start=\"5\">\n<li>Make sure to unmount:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo umount /mnt\n</code></pre>\n<ul>\n<li>\n<p>If everything checks out, reboot the system and you should be prompted to\nenter your <code>user</code> and <code>password</code> to login to a shell to get started.</p>\n</li>\n<li>\n<p>The flake will be placed at <code>/etc/nixos/flake</code> after the install and reboot, I\nchoose to move it to my home directory. Since the file was first in <code>/etc</code>\nyou’ll need to adjust the permissions with something like\n<code>sudo chown -R $USER:$USER ~/flake</code> and then you can work on it without\nprivilege escalation. This requires that you create a group for your user as\ndone in the <code>configuration.nix</code> above.</p>\n</li>\n<li>\n<p>You can check the layout of your btrfs system with:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">sudo btrfs subvolume list /\n</code></pre>\n<h2>Persisting Critical System State</h2>\n<p>The following is a one time operation, we’re just getting it out of the way now.\nThis moves all of the important system state to a persistant location, further\npreparing for impermanence.</p>\n<p>It’s essential that you have first run the <code>nixos-install</code> command to populate\nthese directories before copying them over.</p>\n<pre><code class=\"language-bash\">sudo mkdir -p /mnt/persist/etc\nsudo mkdir -p /mnt/persist/var/lib\nsudo mkdir -p /mnt/persist/var/log\nsudo mkdir -p /mnt/persist/home\nsudo mkdir -p /mnt/persist/root\nsudo cp -a /mnt/etc/. /mnt/persist/etc/\nsudo cp -a /mnt/var/lib/. /mnt/persist/var/lib\nsudo cp -a /mnt/var/log/. /mnt/persist/var/log\nsudo cp -a /mnt/home/. /mnt/persist/home/\nsudo cp -a /mnt/root/. /mnt/persist/root/\n</code></pre>\n<p>Since we are in a live environment, after the install and reboot the <code>/mnt</code>\nprefix will be removed.</p>\n<h2>Reboot</h2>\n<p>Now that everything is done, we can safely reboot and ensure that our LUKS\npassword/passphrase is accepted as well as our userlevel password and username.</p>\n<p>After reboot, you can continue to setup\n<a href=\"https://saylesss88.github.io/installation/enc/sops-nix.html\">Sops Encrypted Secrets</a>\nand\n<a href=\"https://saylesss88.github.io/installation/enc/lanzaboote.html\">Lanzaboote Secure Boot</a></p>\n<ul>\n<li>\n<p>To set up impermanence for this specific layout, follow the link\n<a href=\"https://saylesss88.github.io/installation/enc/encrypted_impermanence.html\">Encrypted Impermanence</a></p>\n</li>\n<li>\n<p><a href=\"https://btrfs.readthedocs.io/en/latest/Subvolumes.html\">BTRFS Subvolumes</a></p>\n</li>\n<li>\n<p><a href=\"https://www.freedesktop.org/software/systemd/man/latest/systemd-cryptenroll.html\">systemd-cryptenroll man page</a></p>\n</li>\n<li>\n<p><a href=\"https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/\">Linux TPM PCR Registry</a></p>\n</li>\n<li>\n<p><a href=\"https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/\">Bypassing FDE with TPM2</a></p>\n</li>\n</ul>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/USB_keyfile.html",
      "url": "https://saylesss88.github.io/installation/enc/USB_keyfile.html",
      "title": "USB Keyfile",
      "content_html": "<h1>USB Stick Keyfile</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>This allows you to use a USB stick for your keyfile, with a backup in case you\nwant or need it. There is a setting <code>fallbackToPassword</code> that protects you in\ncase something fails with the USB key.</p>\n<p>First, I’ll show how to set up a dedicated USB stick for a keyfile. (i.e., one\nthat is only used for this). After that I will show the process of adding the\nkeyfile to a USB stick with existing data on it that you don’t want to lose.</p>\n<p><strong>Generate the keyfile</strong></p>\n<pre><code class=\"language-bash\">sudo dd if=/dev/urandom of=/root/usb-luks.key bs=4096 count=1\n</code></pre>\n<h2>Keyfile Enrollment Methods</h2>\n<p>This is for a dedicated USB stick that we will wipe first then add the key.</p>\n<p>Disko defaults to LUKS2</p>\n<pre><code class=\"language-bash\"># cryptsetup works for both LUKS1 and LUKS2 formats but doesn't work for\n# TPM2, FIDO2, and smartcards\nsudo cryptsetup luksAddKey /dev/disk/by-partlabel/luks /root/usb-luks.key\n</code></pre>\n<p><strong>OR</strong></p>\n<details>\n<summary> ✔️ Click to expand Experimental TPM2 auto-unlock for LUKS </summary>\n<blockquote>\n<p>⚠️ WARNING: Security Implications of TPM2 Auto-Unlock</p>\n</blockquote>\n<blockquote>\n<p>Enabling TPM2 auto-unlock fundamentally changes your system’s security model.\nWhile this feature protects against certain forms of malicious software\ninjection by tying the decryption key to the system’s boot state, it\neliminates the need for a user password at boot. This creates a significant\nrisk if your machine is stolen or seized, do not use this feature if the\nphysical security of your machine is a concern. This is still at a stage where\nyou can expect rough edges and workarounds.</p>\n</blockquote>\n<blockquote>\n<p>⚠️ WARNING: Do NOT use TPM auto-unlock if your CPU is vulnerable to faulTPM!\nAll AMD Zen2 and Zen3 Processors are known to be affected with AMD Zen1 likely\nalso affected and Zen4 unknown! Misconfigurations are also common, do your own\nresearch!</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://ieeexplore.ieee.org/document/10190531\">faulTPM:Exposing AMD fTPMs’ Deepest Secrets</a></p>\n</li>\n<li>\n<p><a href=\"https://www.techpowerup.com/308124/amd-faultpm-exploit-targets-zen-2-and-zen-3-processors\">AMD faulTPM Exploit Targets Zen 2 and Zen 3 Processors</a></p>\n</li>\n</ul>\n<p>You can add an additional layer by encrypting user data, such as individual home\nfolders, with a different mechanism, such as <code>fscrypt-experimental</code> or\n<code>systemd-homed</code>. Or, you can use a TPM pin to benefit from the security\nproperties of the TPM, while avoiding completely unattended unlocking.\n–<a href=\"https://wiki.archlinux.org/title/Trusted_Platform_Module\">Arch Wiki</a></p>\n<p>I am reading that <code>fscrypt</code> is no longer experimental.</p>\n<pre><code class=\"language-nix\">security.pam.enableFscrypt = true;\n</code></pre>\n<pre><code class=\"language-bash\">sudo fscrypt setup --all-users\nsudo mv /home/&lt;user&gt; /home/old&lt;user&gt;\nsudo mkdir /home/&lt;user&gt;\nsudo chown &lt;user&gt;:users /home/&lt;user&gt;\nsudo fscrypt encrypt --source pam_passphrase --user &lt;user&gt; --skip-unlock /home/&lt;user&gt;/\n</code></pre>\n<p>–☝️<a href=\"https://discourse.nixos.org/t/experienced-with-systemd-homed-or-other-encrypted-home/63516/2\">Discourse</a></p>\n<p>It is fairly complex as to how TPM2 auto-unlock can improve security in some\nways, it has to do with how Linux distributions fail to authenticate the boot\nprocess past the initrd.Even with encryption and Secure Boot enabled, the initrd\nstage often remains unverified, meaning a tampered initrd could be substituted\nwithout detection.</p>\n<ul>\n<li><a href=\"https://0pointer.net/blog/brave-new-trusted-boot-world.html\">Brave New Trusted Boot World</a></li>\n</ul>\n<p>TPMs protect secrets by releasing them only if the boot process can be\nauthenticated through “measurements.” During boot, each component involved\n(firmware, bootloader, kernel, etc.) is hashed, and these hashes are extended\ninto special TPM registers called Platform Configuration Registers (PCRs). These\nPCRs hold a cumulative, tamper-evident record of the boot process state.</p>\n<p>If any part of the boot sequence changes (even slightly), the PCR values will\ndiffer from the expected, causing the TPM to refuse to release the bound secret\n(such as a disk decryption key). This ensures that the system only boots or\nunlocks secrets when its software stack is known and trusted, providing strong\nprotection against tampering or unauthorized modifications. The values aren’t\nonly protected by these PCRs but encrypted with a “seed key” that’s generated on\nthe TPM chip itself, and cannot leave the TPM.</p>\n<p>Check TPM support:</p>\n<pre><code class=\"language-bash\">cat /sys/class/tpm/tpm0/device/description\nTPM 2.0 Device\n</code></pre>\n<p>Check for necessary software dependencies:</p>\n<pre><code class=\"language-bash\">systemd-analyze has-tpm2\n</code></pre>\n<p>Find your encrypted partition with <code>lsblk</code>:</p>\n<pre><code class=\"language-bash\">lsblk\n</code></pre>\n<p>First, you need to use the <code>systemd-cryptenroll</code> command to add a TPM2 key to\nyour encrypted LUKS partition. This process binds a key slot on your disk to the\nstate of your TPM2 chip’s PCRs (Platform Configuration Registers).</p>\n<pre><code class=\"language-bash\"># This command adds a new key to the LUKS volume, using a key generated by the TPM2 chip.\n# It binds the key to PCRs 0,2,7,and 15 ensuring the key is only released if the firmware\n# and Secure Boot state of your system is unchanged.\nsudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7+15 /dev/disk/by-partlabel/luks\n</code></pre>\n<p>There are quite a few options for the above command, some use the following with\nless pcrs and a wipe feature:</p>\n<pre><code class=\"language-bash\">sudo systemd-cryptenroll --wipe-slot=tpm2 --tpm2-device=auto --tpm2-pcrs=0+7 /dev/disk/by-partlabel/luks\n</code></pre>\n<ul>\n<li>\n<p>Using less pcrs could prevent breakage but reduces security. Check out the PCR\nDefinitions below and decide if you require additional PCRs or less.</p>\n</li>\n<li>\n<p><code>wipe-slot</code> tells the system to delete any key associated with the TPM2 chip\nfrom the LUKS volume’s keyslot before adding a new one.</p>\n</li>\n</ul>\n<p>You can choose a more complex <code>--tpm2-pcrs</code> for more security but it makes the\nconfiguration more fragile because any legitimate system update altering any\nmeasured component tied to these PCRs will prevent the TPM from releasing the\nkey and lock you out, unless you re-enroll the key with the updated PCR values.</p>\n<ul>\n<li>\n<p><a href=\"https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/\">PCR Definitions</a></p>\n</li>\n<li>\n<p><a href=\"https://0pointer.net/blog/authenticated-boot-and-disk-encryption-on-linux.html\">Authenticated Boot and FDE</a>\nThis article explains the limitations and remedies very well.</p>\n</li>\n</ul>\n<p>That said, I do often see people mention a firmware update breaking their TPM2\nauto-unlock functionality. Keep this in mind and have a backup plan. This is\nalso incompatible with the encrypted impermanence setup shared in this book, the\n<code>boot.initrd.postDeviceCommands</code> conflict.</p>\n<p>Change <code>YourUser</code> to your username and ensure that <code>cryptroot</code> is the name of\nyours, if you followed this books encrypted disko install it should be:</p>\n<pre><code class=\"language-nix\">  # Adds your user to the 'tss' group, allowing you to interact with the TPM\n  users.users.YourUser.extraGroups = [ \"tss\" ];\n  # Enables TPM2 services and tools on your system\n  security.tpm2.enable = true;\n  # Ensure the necessary kernel modules are in the initrd\n  boot.initrd.kernelModules = [\"tpm_tis\"];\n  # switches the initrd to a systemd-based environment, required for TPM2\n  boot.initrd.systemd.enable = true;\n  # ❗ Tell the initrd to use the TPM2 key for the encrypted root\n  boot.initrd.luks.devices.cryptroot = {\n    device = \"/dev/disk/by-partlabel/luks\";\n    # These options tell systemd-cryptsetup to automatically try to unlock the device\n    # using the TPM2 key. 'tpm2-measure=yes' ensures the PCRs are verified but only works if you use one disk\n    crypttabExtraOpts = [\"tpm2-device=auto\" \"tpm2-measure=yes\"];\n    fallbackToPassword = true;\n  };\n  environment.systemPackages = [ pkgs.tpm2-tss ];\n</code></pre>\n<blockquote>\n<p>❗ NOTE: <code>cryptroot</code> needs to match what your encrypted partition is named, I\nhave seen quite a few different names here.</p>\n</blockquote>\n<p>If you use this, you can’t also use the USB Keyfile or the included impermanence\nguide.</p>\n</details>\n<p><strong>Description</strong></p>\n<ul>\n<li>\n<p><code>/dev/disk/by-partlabel/luks</code> refers to your encrypted partition by its\npartition label, which is stable and less likely to change than\n<code>/dev/nvme0n1p2</code></p>\n</li>\n<li>\n<p><code>/root/usb-luks.key</code> is the keyfile we generated.</p>\n</li>\n<li>\n<p>You’ll be prompted to enter your existing LUKS passphrase to authorize adding\nthe new key.</p>\n</li>\n<li>\n<p>Now our LUKS volume will accept both our existing passphrase and the new\nkeyfile (from the USB stick) for unlocking.</p>\n</li>\n</ul>\n<ol>\n<li><strong>Clear Data on USB stick and replace with 0’s</strong></li>\n</ol>\n<pre><code class=\"language-bash\">lsblk\nNAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS\nsda           8:0    1   239M  0 disk\nsdb           8:16   1   1.4M  0 disk  /run/media/jr/7CD1-149A # Example USB mount\nzram0       253:0    0   7.5G  0 disk  [SWAP]\nnvme0n1     259:0    0 476.9G  0 disk\n├─nvme0n1p1 259:1    0   512M  0 part  /boot\n└─nvme0n1p2 259:2    0 476.4G  0 part\n  └─cryptroot 254:0  0 476.4G  0 crypt /persist  # Main Btrfs mount\n                                               # (other subvolumes are within /persist and bind-mounted by impermanence)\n# unplug the device and run lsblk again so your sure\n</code></pre>\n<ol start=\"2\">\n<li>Before wiping you must unmount any mounted partitions:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo umount /dev/sda1\n</code></pre>\n<pre><code class=\"language-bash\"># Overwrite with Zeros (fast, sufficient for most uses)\nsudo dd if=/dev/zero of=/dev/sda bs=4M status=progress\n# Or overwrite with Random Data (More Secure, Slower)\nsudo dd if=/dev/urandom of=/dev/sda bs=4M status=progress\n# Or for the most secure way run multiple passes of\nsudo shred -v -n 3 /dev/sda\n</code></pre>\n<ol start=\"3\">\n<li>Create a New Partition and Format (Optional)</li>\n</ol>\n<pre><code class=\"language-bash\">sudo fdisk /dev/sda\n</code></pre>\n<ol>\n<li>\n<p>Press <code>o</code> to create a new empty DOS partition table (if you are creating\npartitions on a fresh disk or want to wipe existing partitions and start\nover). Be very careful with this step as it will erase all existing\npartition information on the disk.</p>\n</li>\n<li>\n<p>Press <code>n</code> to create a new partition.</p>\n</li>\n</ol>\n<ul>\n<li>\n<p>You will then be prompted for the partition type:</p>\n<ul>\n<li>\n<p><code>p</code> for a primary partition (you can have up to 4 primary partitions)</p>\n</li>\n<li>\n<p><code>e</code> for an extended partition (which can contain logical partitions)</p>\n</li>\n</ul>\n</li>\n<li>\n<p>Next, you’ll be asked for the partition number (e.g., 1, 2, 3, 4).</p>\n</li>\n<li>\n<p>Then, you’ll be asked for the first sector (press Enter to accept the default,\nwhich is usually the first available sector after the previous partition or\nthe beginning of the disk).</p>\n</li>\n<li>\n<p>Finally, you’ll be asked for the last sector or size (you can specify a size\nlike +10G for 10 Gigabytes, +512M for 512 Megabytes, or press Enter to use the\nrest of the available space).</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Press <code>w</code> to write the changes to the partition table and exit fdisk.</li>\n</ol>\n<p>After pressing <code>w</code>, the kernel needs to be aware of the new partition table.\nSometimes this happens automatically, but if you encounter issues, a reboot or a\ncommand like <code>partprobe</code> (if available and needed) can help.</p>\n<p>Formats as FAT32:</p>\n<pre><code class=\"language-bash\">sudo mkfs.vfat /dev/sda1\n# or as ext4\nsudo mkfs.ext4 /dev/sda1\n</code></pre>\n<p>I chose <code>vfat</code> so I ran <code>sudo mkfs.vfat /dev/sda1</code>. In my case this changed the\ndevice path to <code>/run/media/jr/7CD1-149A</code> so it’s important to find your own UUID\nwith the following command:</p>\n<pre><code class=\"language-bash\">sudo blkid /dev/sda1\n/dev/sda1: SEC_TYPE=\"msdos\" UUID=\"B7B4-863B\" BLOCK_SIZE=\"512\" TYPE=\"vfat\" PARTUUID=\"7d1f9d7f-01\"\n</code></pre>\n<ul>\n<li>\n<p>As you can see the above UUID is <code>\"B7B4-863B\"</code></p>\n</li>\n<li>\n<p>Remove and re-insert the USB stick, this ensures the system recognizes the new\npartition and filesystem.</p>\n</li>\n</ul>\n<ol start=\"4\">\n<li>Copy the keyfile to your USB Stick</li>\n</ol>\n<pre><code class=\"language-bash\">sudo cp /root/usb-luks.key /run/media/jr/B7B4-863B/\nsync\n</code></pre>\n<ol start=\"5\">\n<li>Update your NixOS Configuration</li>\n</ol>\n<p>Note the output of <code>blkid /dev/sda1</code> and if you have a backup device list that\nalso:</p>\n<p>The following is from the wiki edited for my setup, it was created by Tzanko\nMatev:</p>\n<pre><code class=\"language-nix\">let\n  PRIMARYUSBID = \"B7B4-863B\";\n  BACKUPUSBID = \"Ventoy\";\nin {\n\n  boot.initrd.kernelModules = [\n    \"uas\"\n    \"usbcore\"\n    \"usb_storage\"\n    \"vfat\"\n    \"nls_cp437\"\n    \"nls_iso8859_1\"\n  ];\n\n  boot.initrd.postDeviceCommands = lib.mkBefore ''\n    mkdir -p /key\n    sleep 2\n    mount -n -t vfat -o ro $(findfs UUID=${PRIMARYUSBID}) /key || \\\n    mount -n -t vfat -o ro $(findfs UUID=${BACKUPUSBID}) /key || echo \"No USB key found\"\n  '';\n\n  boot.initrd.luks.devices.cryptroot = {\n    device = \"/dev/disk/by-partlabel/luks\";\n    keyFile = \"/key/usb-luks.key\";\n    fallbackToPassword = true;\n    allowDiscards = true;\n    preLVM = false; # Crucial!\n  };\n}\n</code></pre>\n<p>If you have issues or just want to remove the key take note of the path used to\nadd it so you don’t have to enter the whole key:</p>\n<pre><code class=\"language-bash\">sudo cryptsetup luksRemoveKey /dev/disk/by-partlabel/luks --key-file /root/usb-luks.key\n</code></pre>\n<ol start=\"6\">\n<li>Securely Remove the Keyfile from Your System:</li>\n</ol>\n<pre><code class=\"language-bash\">sudo shred --remove --zero /root/usb-luks.key\n</code></pre>\n<h2>Instructions for Using a USB Stick with Existing Data</h2>\n<ol>\n<li>Generate the Keyfile</li>\n</ol>\n<pre><code class=\"language-bash\">sudo dd if=/dev/urandom of=/root/usb-luks.key bs=4096 count=1\n</code></pre>\n<ol start=\"2\">\n<li>Add the Keyfile to your LUKS Volume</li>\n</ol>\n<pre><code class=\"language-bash\">sudo cryptsetup luksAddKey /dev/disk/by-partlabel/luks /root/usb-luks.key\n</code></pre>\n<p>(enter your existing passphrase when prompted)</p>\n<ol start=\"3\">\n<li>Copy the Keyfile to the USB Stick</li>\n</ol>\n<ul>\n<li>\n<p>Plug in the USB Stick and note its mount point\n(e.g.,<code>/run/media/$USER/YourLabel</code>)</p>\n</li>\n<li>\n<p>Copy the keyfile:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">sudo cp /root/usb-luks.key /run/media/$USER/YourLabel/\nsync\n</code></pre>\n<ul>\n<li>\n<p>You run the above as 2 commands, the second being <code>sync</code>.</p>\n</li>\n<li>\n<p>You can rename it if you wish (e.g., <code>luks.key</code>)</p>\n</li>\n</ul>\n<ol start=\"4\">\n<li>Securely Delete the Local Keyfile</li>\n</ol>\n<pre><code class=\"language-bash\">sudo shred --remove --zero /root/usb-luks.key\n</code></pre>\n<ul>\n<li>You need to ensure the keyfile is accessible in the initrd. Since automounting\n(like <code>/run/media/...</code>) does not happen in <code>initrd</code>, you must manually mount\nthe USB in the <code>initrd</code> using its <code>UUID</code> or label.</li>\n</ul>\n<p>Find the USB Partition UUID:</p>\n<pre><code class=\"language-bash\">lsblk -o NAME,UUID\n# or\nblkid /dev/sda1\n</code></pre>\n<p>Suppose the UUID is <code>B7B4-863B</code></p>\n<p>Add to your <code>configuration.nix</code>:</p>\n<pre><code class=\"language-nix\">boot.initrd.kernelModules = [ \"usb_storage\" \"vfat\" \"nls_cp437\" \"nls_iso8859_1\" ];\n\nboot.initrd.postDeviceCommands = lib.mkBefore ''\n  mkdir -p /key\n  sleep 1\n  mount -n -t vfat -o ro $(findfs UUID=B7B4-863B) /key || echo \"USB not found\"\n'';\n\nboot.initrd.luks.devices.cryptroot = {\n  device = \"/dev/disk/by-partlabel/luks\";\n  keyFile = \"/key/usb-luks.key\"; # or whatever you named it\n  fallbackToPassword = true;\n  allowDiscards = true;\n};\n</code></pre>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/encrypted_impermanence.html",
      "url": "https://saylesss88.github.io/installation/enc/encrypted_impermanence.html",
      "title": "Encrypted BTRFS Impermanence",
      "content_html": "<h1>Encrypted Impermanence</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<blockquote>\n<p>❗ Important Note: This guide details a setup involving encrypted partitions\nand impermanent NixOS. While powerful, such configurations require careful\nattention to detail. Incorrect steps, especially concerning encryption keys or\npersistent data paths, can lead to <strong>permanent data loss</strong>. Please read all\ninstructions thoroughly before proceeding and consider backing up any critical\ndata beforehand. This has only been tested with the disk layout described in\n<a href=\"https://saylesss88.github.io/installation/encrypted_manual.html\">Encrypted Setups</a></p>\n</blockquote>\n<p>As a system operates, it gradually accumulates state on its root partition. This\nstate is stored in various directories such as <code>/etc</code> and <code>/var</code>, capturing all\nthe configuration changes, logs, and other modifications—whether they’re\nwell-documented or the result of ad-hoc adjustments made while setting up and\nrunning services.</p>\n<p><strong>Impermanence</strong>,in the context of operating systems, refers to a setup where\nthe majority of the system’s root filesystem (<code>/</code>) is reset to a pristine state\non every reboot. This means any changes made to the system (e.g., installing new\npackages, modifying system files outside of configuration management, creating\ntemporary files) are discarded upon shutdown or reboot.</p>\n<p>Having an impermanent root and <code>/tmp</code> has some security benefits as well. By\nreducing your persistent footprint you reduce your chance of leaving behind\nsensitive activity or data. Since Nix can boot with only <code>/nix</code> and <code>/boot</code>,\nexperienced users familiar with “stateless” systems can take advantage of this\nsmaller attack surface.</p>\n<p>Although this setup does not use <code>/tmp</code> as the root filesystem, the root itself\nis restored to its original state upon each reboot, as it was at installation.\nHowever, by configuring <code>/tmp</code> to reside in RAM, you ensure that temporary files\nincluding sensitive data like passwords are stored only in volatile memory and\nare automatically cleared on shutdown or reboot. This significantly enhances the\nsecurity of temporary data by preventing it from ever being written to disk.</p>\n<h3>Getting Started</h3>\n<ol>\n<li>Add impermanence to your <code>flake.nix</code>. You will change the <code>hostname</code> in the\nflake to match your <code>networking.hostName</code>.</li>\n</ol>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    disko.url = \"github:nix-community/disko/latest\";\n    disko.inputs.nixpkgs.follows = \"nixpkgs\";\n    impermanence.url = \"github:nix-community/impermanence\";\n  };\n\n  outputs = inputs@{ nixpkgs, ... }: {\n    nixosConfigurations = {\n      hostname = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          inputs.disko.nixosModules.disko\n          inputs.impermanence.nixosModules.impermanence\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<ol start=\"2\">\n<li>Discover where your root subvolume is located with <code>findmnt</code>:</li>\n</ol>\n<p>If you followed the\n<a href=\"https://saylesss88.github.io/installation/encrypted_manual.html\">Encrypted Setups</a>\nguide, your encrypted subvolume should be located at:\n<code>/dev/mapper/cryptroot /mnt</code></p>\n<ul>\n<li>Your encrypted Btrfs partition, once unlocked by LUKS, will be available at\n<code>/dev/mapper/cryptroot</code> as configured here in the <code>disk-config.nix</code>:</li>\n</ul>\n<pre><code class=\"language-nix\"># disk-config2.nix\n# ... snip ...\n            luks = {\n              size = \"100%\";\n              label = \"luks\";\n              content = {\n                type = \"luks\";\n                name = \"cryptroot\";\n                content = {\n# ... snip ...\n</code></pre>\n<p>Double check that the paths exist:</p>\n<pre><code class=\"language-bash\">cd /dev/mapper/crypt&lt;TAB&gt;  # autocomplete should fill out /dev/mapper/cryptroot\n</code></pre>\n<ol start=\"3\">\n<li>Create an <code>impermanence.nix</code>:</li>\n</ol>\n<p>Now, create a new file named <code>impermanence.nix</code> in your configuration directory\n(i.e. your flake directory). This file will contain all the specific settings\nfor your impermanent setup, including BTRFS subvolume management and persistent\ndata locations. Since this file is right next to your <code>configuration.nix</code>,\nyou’ll just add an <code>imports = [ ./impermanence.nix ]</code> to your\n<code>configuration.nix</code> apply it to your configuration.</p>\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  ...\n}: {\n  boot.initrd.postDeviceCommands = lib.mkAfter ''\n    echo \"Rollback running\" &gt; /mnt/rollback.log\n     mkdir -p /mnt\n     mount -t btrfs /dev/mapper/cryptroot /mnt\n\n     # Recursively delete all nested subvolumes inside /mnt/root\n     btrfs subvolume list -o /mnt/root | cut -f9 -d' ' | while read subvolume; do\n       echo \"Deleting /$subvolume subvolume...\" &gt;&gt; /mnt/rollback.log\n       btrfs subvolume delete \"/mnt/$subvolume\"\n     done\n\n     echo \"Deleting /root subvolume...\" &gt;&gt; /mnt/rollback.log\n     btrfs subvolume delete /mnt/root\n\n     echo \"Restoring blank /root subvolume...\" &gt;&gt; /mnt/rollback.log\n     btrfs subvolume snapshot /mnt/root-blank /mnt/root\n\n     umount /mnt\n  '';\n\n  environment.persistence.\"/persist\" = {\n    directories = [\n      \"/etc\"\n      \"/var/spool\"\n      \"/srv\"\n      \"/etc/NetworkManager/system-connections\"\n      \"/var/lib/bluetooth\"\n    ];\n    files = [\n      # \"/etc/machine-id\"\n      # Add more files you want to persist\n    ];\n  };\n\n# optional quality of life setting\n  security.sudo.extraConfig = ''\n    Defaults lecture = never\n  '';\n}\n</code></pre>\n<ul>\n<li><code>/mnt/rollback.log</code>: this log will be available during the boot process for\ndebugging if the rollback fails, but won’t persist.</li>\n</ul>\n<p>With the above impermanence script, the btrfs subvolumes are deleted recursively\nand replaced with the <code>root-blank</code> snapshot we took during the install.</p>\n<p>I have commented out <code>\"/etc/machine-id\"</code> because we already copied over all of\nthe files to their persistent location and the above setting would work once and\nthen cause a conflict.</p>\n<h2>configuration.nix changes</h2>\n<pre><code class=\"language-nix\"># configuration.nix\n  boot.initrd.luks.devices = {\n    cryptroot = {\n      device = \"/dev/disk/by-partlabel/luks\";\n      allowDiscards = true;\n      preLVM = true;\n    };\n  };\n</code></pre>\n<ul>\n<li>This defines how your system’s initial ramdisk (<code>initrd</code>) should handle a\nspecific encrypted disk during the boot process. It helps with timing and is a\nmore robust way of telling Nix that we are using an encrypted disk.</li>\n</ul>\n<p>The following is optional to enable <code>autoScrub</code> for btrfs, the wiki shows\n<code>interval = \"monthly\";</code> FYI.</p>\n<pre><code class=\"language-nix\"># configuration.nix\n  services.btrfs.autoScrub = {\n    enable = true;\n    interval = \"weekly\";\n    fileSystems = [\"/\"];\n  };\n</code></pre>\n<ul>\n<li>Remember to ensure that your <code>hostname</code> in your <code>configuration.nix</code> matches\nthe <code>hostname</code> in your <code>flake.nix</code>.</li>\n</ul>\n<h3>Applying Your Impermanence Configuration</h3>\n<p>Once you have completed all the steps and created or modified the necessary\nfiles (<code>flake.nix</code>, <code>impermanence.nix</code>), you need to apply these changes to your\nNixOS system.</p>\n<ol>\n<li>Navigate to your NixOS configuration directory (where your <code>flake.nix</code> is\nlocated).</li>\n</ol>\n<pre><code class=\"language-bash\">cd /path/to/your/flake\n</code></pre>\n<ol start=\"2\">\n<li>Rebuild and Switch: Execute the <code>nixos-rebuild switch</code> command. This command\nwill:</li>\n</ol>\n<ul>\n<li>\n<p>Evaluate your <code>flake.nix</code> and the modules it imports (including your new\n<code>impermanence.nix</code>).</p>\n</li>\n<li>\n<p>Build a new NixOS system closure based on your updated configuration.</p>\n</li>\n<li>\n<p>Activate the new system configuration, making it the current running system.</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --flake .#hostname # Replace 'hostname' with your actual system hostname\n</code></pre>\n<ol start=\"3\">\n<li>Perform an Impermanence Test (Before Reboot):</li>\n</ol>\n<ul>\n<li>Before you reboot, create a temporary directory and file in a non-persistent\nlocation. Since you haven’t explicitly added <code>/imperm_test</code> to your\n<code>environment.persistence.\"/persist\"</code> directories, this file should not survive\na reboot.</li>\n</ul>\n<pre><code class=\"language-bash\">mkdir /imperm_test\necho \"This should be Gone after Reboot\" | sudo tee /imperm_test/testfile\nls -l /imperm_test/testfile # Verify the file exists\ncat /imperm_test/testfile # Verify content\n</code></pre>\n<ol start=\"4\">\n<li>Reboot Your System: For the impermanence setup to take full effect and for\nyour root filesystem to be reset for the first time, you must reboot your\nmachine.</li>\n</ol>\n<pre><code class=\"language-bash\">sudo reboot\n</code></pre>\n<ol start=\"5\">\n<li>Verify Impermanence (After Reboot):</li>\n</ol>\n<ul>\n<li>After the system has rebooted, check if the test directory and file still\nexist:</li>\n</ul>\n<pre><code class=\"language-bash\">ls -l /imperm_test/testfile\n</code></pre>\n<p>You should see an output like <code>ls: cannot access '/imperm_test/testfile'</code>: No\nsuch file or directory. This confirms that the <code>/imperm_test</code> directory and its\ncontents were indeed ephemeral and were removed during the reboot process,\nindicating your impermanence setup is working correctly!</p>\n<p>Your system should now come up with a fresh root filesystem, and only the data\nspecified in your <code>environment.persistence.\"/persist\"</code> configuration will be\npersistent.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/sops-nix.html",
      "url": "https://saylesss88.github.io/installation/enc/sops-nix.html",
      "title": "Sops-Nix",
      "content_html": "<h1>Sops-Nix encrypted secrets</h1>\n<details>\n<summary> Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><a href=\"https://github.com/getsops/sops?ref=blog.gitguardian.com\">SOPS</a>, short for\n<strong>S</strong>ecrets<strong>OP</strong>eration<strong>S</strong>, is an editor of encrypted files that supports\nquite a few BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault,\nage, and PGP.</p>\n<p>Managing secrets—like API keys, SSH deploy keys, and password hashes is a\ncritical part of system configuration, but it’s also one of the trickiest to do\nsecurely and reproducibly. Traditionally, secrets might be stored in ad hoc\nlocations, referenced by absolute paths, or managed manually outside of version\ncontrol. This approach makes it hard to share, rebuild, or audit your\nconfiguration, and increases the risk of accidental leaks or inconsistencies\nbetween systems.</p>\n<p><code>sops-nix</code> solves these problems by integrating Mozilla SOPS directly into your\nNixOS configuration. Instead of relying on hardcoded file paths or copying\nsecrets around, you declare your secrets in your Nix code, encrypt them with\nstrong keys, and let <code>sops-nix</code> handle decryption and placement at activation\ntime.</p>\n<p>Encryption with strong keys, as used by sops-nix, makes brute force attacks\ncomputationally unfeasible with current technology—the time and resources\nrequired to try every possible key would be astronomically high. However, this\nprotection relies on using strong, secret keys and good security practices;\nadvances in technology or poor key management can weaken this defense.</p>\n<blockquote>\n<p>❗ <strong>CRITICAL SECURITY NOTE:</strong> While the encryption itself is robust, this\nprotection fundamentally relies on using <strong>strong, secret keys</strong> and\n<strong>diligent security practices</strong>. If your PGP passphrase is weak, your Age\nprivate key is easily guessable, or the cleartext secret itself is very short\nand has low entropy (e.g., “12345”, “true”, “admin”), an attacker might be\nable to compromise your secrets regardless of the encryption.</p>\n</blockquote>\n<ol>\n<li>Add sops to your <code>flake.nix</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">{\n  inputs.sops-nix.url = \"github:Mic92/sops-nix\";\n  inputs.sops-nix.inputs.nixpkgs.follows = \"nixpkgs\";\n\n  outputs = { self, nixpkgs, sops-nix }: {\n    # change `yourhostname` to your actual hostname\n    nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {\n      # customize to your system\n      system = \"x86_64-linux\";\n      modules = [\n        ./configuration.nix\n        sops-nix.nixosModules.sops\n      ];\n    };\n  };\n}\n</code></pre>\n<ol start=\"2\">\n<li>Add <code>sops</code> and <code>age</code> to your <code>environment.systemPackages</code>:</li>\n</ol>\n<pre><code class=\"language-nix\">environment.systemPackages = [\n    pkgs.sops\n    pkgs.age\n];\n</code></pre>\n<ol start=\"3\">\n<li>Generate a key (This is your <strong>private key</strong> and <strong>MUST NEVER BE COMMITTED TO\nGIT OR SHARED</strong>):</li>\n</ol>\n<pre><code class=\"language-bash\">mkdir -p ~/.config/sops/age\nage-keygen -o ~/.config/sops/age/keys.txt\n</code></pre>\n<p>To get the Public Keys Value, run the following command:</p>\n<pre><code class=\"language-bash\">age-keygen -y ~/.config/sops/age/keys.txt\nage12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl\n</code></pre>\n<p>Copy the <code>age</code> value it gives you back.</p>\n<ol start=\"4\">\n<li>Create a <code>.sops.yaml</code> in the same directory as your <code>flake.nix</code>:</li>\n</ol>\n<pre><code class=\"language-yaml\"># .sops.yaml\nkeys:\n  # Your personal age public key (from age-keygen -y ~/.config/sops/age/keys.txt)\n  - &amp;personal_age_key age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl\n\n  # You can also use PGP keys if you prefer, but age is often simpler\n  # - &amp;personal_pgp_key 0xDEADBEEFCAFE0123\n\ncreation_rules:\n  # This rule applies to any file named 'secrets.yaml' directly in the 'secrets/' directory\n  # or 'secrets/github-deploy-key.yaml' etc.\n  - path_regex: \"secrets/.*\\\\.yaml$\"\n    key_groups:\n      - age:\n          - *personal_age_key\n        # Add host keys for decryption on the target system\n        # sops-nix will automatically pick up the system's SSH host keys\n        # as decryption keys if enabled in your NixOS config.\n        # So you typically don't list them explicitly here unless you\n        # want to restrict it to specific fingerprints, which is rare.\n        # This part ensures your *personal* key can decrypt it.\n</code></pre>\n<p>Save it and move on, this file and <code>sops.nix</code> are safe to version control.</p>\n<ol start=\"5\">\n<li>sops-nix’s automatic decryption feature using system SSH host keys only works\nwith ed25519 host keys for deriving Age decryption keys. Therefore, for\nsystem decryption, ensure your using ed25519 not rsa keys:</li>\n</ol>\n<pre><code class=\"language-bash\">ssh-keygen -t ed25519 -C \"your_email@example.com\"\n# for multiple keys run something like\nssh-keygen -t ed25519 -f ~/nix-book-deploy-key -C \"deploy-key-nix-book-repo\"\n</code></pre>\n<ol start=\"6\">\n<li>Copy the <strong>PRIVATE</strong> key for each and add them to your secrets directory:</li>\n</ol>\n<p>While in your flake directory:</p>\n<pre><code class=\"language-bash\">mkdir secrets\nsops secrets/github-deploy-key.yaml  # For your github ssh key\n</code></pre>\n<p>When you call a <code>sops</code> command, it will handle the encryption/decryption\ntransparently and open the cleartext file in an editor.</p>\n<p>Editing will happen in the editor that <code>$SOPS_EDITOR</code> or <code>$EDITOR</code> is set to,\nsops will wait for the editor to exit, and then try to reencrypt the file.</p>\n<p>The above command will open a default sops <code>github-deploy-key.yaml</code> in your\n<code>$EDITOR</code>:</p>\n<p>Erase the default <code>sops</code> filler and type <code>github_deploy_key_ed25519: |</code>, move\nyour cursor 1 line down and type <code>:r ~/.ssh/id_ed25519</code> to read the private key\ninto the file and repeat as needed.</p>\n<pre><code class=\"language-yaml\">github_deploy_key_ed25519: |\n  -----BEGIN OPENSSH PRIVATE KEY-----\n  ...\n  -----END OPENSSH PRIVATE KEY-----\n\ngithub_deploy_key_ed25519_nix-book: |\n  -----BEGIN OPENSSH PRIVATE KEY-----\n  ...\n  -----END OPENSSH PRIVATE KEY-----\n</code></pre>\n<p>The <code>-----BEGIN</code> and the rest of the private key <strong>must</strong> be indented 2 spaces</p>\n<p>Ensure sops can decrypt it:</p>\n<pre><code class=\"language-bash\">sops -d secrets/github-deploy-key.yaml\n</code></pre>\n<blockquote>\n<p>❗ WARNING: Only ever enter your private keys through the <code>sops</code> command. If\nyou forget and paste them in without the <code>sops</code> command then run <code>git add</code> at\nany point, your git history will have contained an unencrypted secret which is\na nono. Always use the <code>sops</code> command when dealing with files in the <code>secrets</code>\ndirectory, save the file and inspect that it is encrypted on save. If not\nsomething went wrong with the <code>sops</code> process, <strong>do not add it to Git</strong>. If you\ndo, you will be required to rewrite your entire history which can be bad if\nyou’re collaborating with others. <code>git-filter-repo</code> is one such solution that\nrewrites your history. Just keep this in mind. This happens because Git has a\nprotection that stops you from doing stupid things.</p>\n</blockquote>\n<p>Generate an encrypted password hash with:</p>\n<pre><code class=\"language-bash\">mkpasswd --method=yescrypt &gt; /tmp/password-hash.txt\n# Enter your chosen password and copy the encrypted hash it gives you back\n</code></pre>\n<pre><code class=\"language-bash\">sops secrets/password-hash.yaml      # For your `hashedPasswordFile`\n</code></pre>\n<p>The above command will open your <code>$EDITOR</code> with the file <code>password-hash.yaml</code>,\nadd the following content to it. Replace <code>PasteEncryptedHashHere</code> with the\noutput of the <code>mkpasswd</code> command above:</p>\n<p>Delete the default <code>sops</code> filler, type <code>password_hash:</code> and leave your cursor\nafter the <code>:</code> and type <code>:r /tmp/password-hash.txt</code></p>\n<pre><code class=\"language-yaml\">password_hash: PasteEncryptedHashHere\n</code></pre>\n<p>Ensure sops can decrypt it:</p>\n<pre><code class=\"language-bash\">sops -d secrets/password-hash.yaml\n</code></pre>\n<ol start=\"7\">\n<li>Create a <code>sops.nix</code> and import it or add this directly to your\n<code>configuration.nix</code>:</li>\n</ol>\n<p>My <code>sops.nix</code> is located at <code>~/flake/hosts/hostname/sops.nix</code> and the secrets\ndirectory is located at <code>~/flake/secrets</code> so the path from <code>sops.nix</code> to\n<code>secrets/pasword-hash.yaml</code> would be <code>../../secrets/password-hash.yaml</code></p>\n<p>Another step you can take is to copy your key to a persistent location,\npreparing for impermanence:</p>\n<pre><code class=\"language-bash\">sudo mkdir /persist/sops/age\nsudo cp ~/.config/sops/age/keys.txt /persist/sops/age/keys.txt\n</code></pre>\n<p>Then you would change the <code>age.keyFile = \"/persist/sops/age/keys.txt\"</code> to match\nthis location below.</p>\n<pre><code class=\"language-nix\"># ~/flake/hosts/magic/sops.nix  # magic is my hostname\n# hosts/magic/ is also where my configuration.nix is\n{...}: {\n  sops = {\n    defaultSopsFile = ../../.sops.yaml; # Or the correct path to your .sops.yaml\n    # Don't mix sshKeyPaths and keyFile\n    age.sshKeyPaths = [];\n    age.keyFile = \"/persist/sops/age/keys.txt\";\n\n    secrets = {\n      \"password_hash\" = {\n        sopsFile = ../../secrets/password-hash.yaml; # &lt;-- Points to your password hash file\n        owner = \"root\";\n        group = \"root\";\n        mode = \"0400\";\n        neededForUsers = true;\n      };\n      \"github_deploy_key_ed25519_nix-book\" = {\n        sopsFile = ../../secrets/github-deploy-key.yaml;\n        key = \"github_deploy_key_ed25519_nix-book\";\n        owner = \"root\";\n        group = \"root\";\n        mode = \"0400\";\n      };\n      \"github_deploy_key_ed25519\" = {\n        sopsFile = ../../secrets/github-deploy-key.yaml;\n        key = \"github_deploy_key_ed25519\";\n        owner = \"root\";\n        group = \"root\";\n        mode = \"0400\";\n      };\n    };\n  };\n}\n</code></pre>\n<p>Import <code>sops.nix</code> into your <code>configuration.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\"># configuration.nix\nimports = [\n  ./sops.nix # Assuming sops.nix is in the same directory as configuration.nix, adjust path as needed\n  # ... other imports\n];\n</code></pre>\n<blockquote>\n<p>❗ NOTE: You may see in the sops quickstart guide that if you’re using\nimpermanence, the key used for secret decryption (<code>sops.age.keyFile</code>) must be\nin a persistent directory, loaded early enough during the boot process. If you\nare using the btrfs subvolume layout you don’t need to worry about this\nbecause your home will be on its own partition when only the root partition is\nwiped on reboot. Adding <code>neededForUsers = true;</code> tells <code>sops-nix</code> to decrypt\nand make that secret available earlier in the boot process specifically,\nbefore user and group accounts are created.</p>\n</blockquote>\n<p>You typically use <code>age.sshKeyPaths</code> for <strong>system-level secrets</strong> with a\npersistent SSH host key</p>\n<p>For <strong>user-level secrets</strong>, use <code>age.keyFile</code> pointing to your Age private key,\nstored in a safe persistent location.</p>\n<p>For reproducibility, keep your key files in a persistent, predictable path and\ndocument which keys are used for which secrets in your <code>.sops.yaml</code>.</p>\n<p>If you don’t need both <code>age.keyFile</code> and <code>age.sshKeyPaths</code> it can reduce\ncomplexity to use one or the other. Although most people may choose one, it’s\nnot bad to use both it just adds complexity.</p>\n<p>And finally use the password-hash for your <code>hashedPasswordFile</code> for your user,\nmy user is <code>jr</code> so I added this:</p>\n<pre><code class=\"language-nix\"># ... snip ...\n    users.users = {\n      # ${username} = {\n      jr = {\n        homeMode = \"755\";\n        isNormalUser = true;\n        # description = userVars.gitUsername;\n        hashedPasswordFile = config.sops.secrets.password_hash.path;\n  # ...snip...\n</code></pre>\n<p>By integrating SOPS with NixOS through <code>sops-nix</code>, you gain a modern, secure,\nand reproducible way to manage sensitive secrets. Unlike traditional approaches\nwhere secrets are often scattered in ad hoc locations, referenced by absolute\npaths, or managed outside version control, <code>sops-nix</code> keeps your secrets\nencrypted, declarative, and version-control friendly.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nix/index.html",
      "url": "https://saylesss88.github.io/nix/index.html",
      "title": "Readme1",
      "content_html": "<h1>Hardening README</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>📌 <strong>How to Use This Guide</strong></p>\n<p><strong>Read warnings</strong>: Advanced hardening can break compatibility or cause data\nloss! Pause and research before enabling anything not listed above unless you\nunderstand the consequences.</p>\n<p><strong>Hardening NixOS</strong>:</p>\n<p>🔷 Start Here:</p>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html\">Hardening NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/hardening_networking.html\">Hardening Networking</a></p>\n</li>\n</ul>\n<hr />\n<p><strong>Additional security/hardening topics</strong></p>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/browsing_security.html\">Browser/Browsing Security/Privacy</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/gpg-agent.html\">GnuPG gpg-agent</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/whonix_kvm.html\">Whonix KVM on NixOS</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/kvm.html\">Running NixOS in a secureblue VM</a></p>\n</li>\n</ul>\n<h2>Getting Started</h2>\n<p>There is a lot covered in this guide which can get overwhelming when trying to\ndecide what is worth implementing. Here, I will list some common recommendations\nthat most users should follow to harden their stance.</p>\n<blockquote>\n<p>“The major problem with current systems is their inability to provide\neffective isolation between various programs running on one machine. E.g. if\nthe user’s Web browser gets compromised (due to a bug exploited by a malicious\nweb site), the OS is usually unable to protect other user’s applications and\ndata from also being compromised.”–Qubes arch-spec</p>\n</blockquote>\n<h2>Threat Modeling</h2>\n<p>You should always start by conducting a personal threat assesment to identify\npotential threats and vulnerabilities that you need to develop strategies to\ndefend against.</p>\n<p>Threat modeling in computing involves evaluating the security risks to your\ncomputer or network. It helps uncover possible threats and weaknesses so you can\ncreate plans to safeguard your systems and data effectively. By examining\nvarious attack scenarios, you can anticipate potential cyber threats and better\nprotect your digital resources.</p>\n<p>It’s not possible to protect yourself against every attack(er), focus on the\nmost probable threats to your specific situation.</p>\n<ul>\n<li>\n<p><a href=\"https://ssd.eff.org/playlist/want-security-starter-pack\">EFF Security Starter Pack</a></p>\n</li>\n<li>\n<p><a href=\"https://ssd.eff.org/module/your-security-plan\">EFF Your Security Plan</a></p>\n</li>\n<li>\n<p><a href=\"https://www.kicksecure.com/wiki/Threat_Modeling\">Kicksecure Computer Security Threat Modeling</a></p>\n</li>\n</ul>\n<h3>Baseline Hardening</h3>\n<p>Before diving into advanced or specialized hardening, apply these baseline\nsecurity measures suitable for all NixOS users. These settings help protect your\nsystem with minimal risk of breaking workflows or causing admin headaches.</p>\n<p>There is something to be said about the window manager you use. GNOME, KDE\nPlasma, and Sway secure privileged Wayland protocols like screencopy. This means\nthat on environments outside of GNOME, KDE, and Sway, applications can access\nscreen content of the entire desktop. This implicitly includes the content of\nother applications. It’s primarily for this reason that Silverblue, Kinoite, and\nSericea images are recommended. COSMIC has plans to fix this.\n–<a href=\"https://secureblue.dev/images\">secureblue Images</a></p>\n<p>Secureblue recommends disabling Xwayland and finding alternatives for those apps\nas well as disabling <code>xdg-desktop-portal-wlr</code>, this is because the wlroots\ndesktop portal reintroduces the screencopy vulnerability.</p>\n<ul>\n<li>\n<p>Use Disk Encryption (LUKS) to protect your data at rest.</p>\n</li>\n<li>\n<p>Keep your system up to date (update regularly).</p>\n</li>\n<li>\n<p>Use strong, unique passwords. To generate one from the command-line, there is\n<code>pkgs.diceware</code>. Generate a password with: <code>diceware -n 12 -w en_eff</code>, add\nspaces between the words for higher entropy.</p>\n<ul>\n<li><a href=\"https://www.kicksecure.com/wiki/Passwords#Password_Generation\">Kicksecure Password_Generation</a></li>\n</ul>\n</li>\n<li>\n<p>Avoid reusing passwords, use a password manager.</p>\n</li>\n<li>\n<p>Avoid storing files directly in the root home folder (i.e., <code>/home/user</code>),\ncreate sub-folders instead.(i.e., Instead of creating <code>~/notes.txt</code>, create\n<code>~/my-notes/notes.txt</code> or <code>~/Documents/notes.txt</code>).</p>\n<ul>\n<li>\n<p>If you are able to implement a Mandatory Access Control framework, there are\nmore sub-folders that should be avoided such as <code>~/Downloads</code>. Another\nreason to use non-default sub-dirs is to avoid typos deleting important\nfiles.</p>\n</li>\n<li>\n<p>Home-Manager has an option <code>xdg.userDirs.enable</code></p>\n</li>\n</ul>\n</li>\n</ul>\n<pre><code class=\"language-nix\"># home.nix or equivalent\n{\n  xdg.userDirs.enable = true;\n  xdg.userDirs.createDirectories = true;\n  # Optionally create non-default sub-dirs\n  # xdg.userDirs.documents = \"/home/jr/my-documents\";\n  # xdg.userDirs.download = \"/home/jr/my-downloads\";\n}\n</code></pre>\n<ul>\n<li>\n<p>The XDG Base Directory Specification defines a consistent way for apps and\ndesktops to store and find files. It helps prevent “dotfile clutter” by\ndirecting application files into clear, organized locations.</p>\n</li>\n<li>\n<p>Only enable what you use, and actively disable what’s no longer in use.</p>\n</li>\n<li>\n<p>Enable at least a basic firewall, a more complex firewall example that\nutilizes nftables is shared in the\n<a href=\"https://saylesss88.github.io/nix/hardening_networking.html\">Hardening Networking Chapter</a></p>\n</li>\n</ul>\n<p>Although the firewall is enabled by default on NixOS, let’s be explicit about\nit, add the following to your <code>configuration.nix</code> or equivalent:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n# this denies incoming connections but allows outgoing and established connections\nnetworking.firewall.enable = true;\n</code></pre>\n<p>Many services provide an option to open the required firewall ports\nautomatically. For example:</p>\n<pre><code class=\"language-nix\">services.tor.openFirewall = true;\n</code></pre>\n<p>This prevents you from having to manually open ports</p>\n<p><strong>Audit and remove local user accounts that are no longer needed</strong>: Regularly\nreview and remove unused or outdated accounts to reduce your system’s attack\nsurface, improve compliance, and ensure only authorized users have access. The\nfollowing setting ensures that user (and group) management is fully declarative:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n# All users must be declared\nusers.mutableUsers = false;\n</code></pre>\n<p>With <code>users.mutableUsers = false;</code>, all non-declaratively managed (imperative)\nuser management including creation, modification, or password changes will fail\nor be reset on rebuild. User and group definitions become entirely controlled by\nyour system configuration for maximum reproducibility and security. If you need\nto add, remove, or modify users, you must do so in your <code>configuration.nix</code> and\nrebuild the system.</p>\n<p>Don’t log in as <code>root</code>, it’s unnecessary.</p>\n<p>Commands that require <code>root</code> permissions should be run individually using <code>sudo</code>\nin all cases. Avoid logging in as <code>root</code> &amp; using <code>sudo su</code>.</p>\n<p>Never run GUI applications as <code>root</code>. If there is a legitimate reason for doing\nthis, use <code>lxsudo</code> instead.</p>\n<hr />\n<blockquote>\n<p>NOTE: There is mention of making\n<a href=\"https://github.com/nikstur/userborn\">userborn</a> the default for NixOS in the\nfuture. It can be more secure by prohibiting UID/GID re-use and giving\nwarnings about insecure password hashing schemes.</p>\n</blockquote>\n<p>I have personally had nothing but problems with <code>userborn</code> and find the docs\nextremely lacking, you need to read the source code to figure anything out which\nis ridiculous. I don’t personally use this but if you figure it out, more power\nto ya.</p>\n<p>To enable <code>userborn</code>, just add the following to your <code>configuration.nix</code> or\nequivalent:</p>\n<pre><code class=\"language-nix\"># users.nix\n{pkgs,...}:{\nservices.userborn = {\n    enable = true;\n    # Only needed if `/etc` is immutable\n    # passwordFilesLocation = \"/var/lib/nixos/userborn\"\n};\n    users.users = {\n       \"newuser\" = {\n         homeMode = \"755\";\n         uid = 1000;\n         isNormalUser = true;\n         description = \"New user account\";\n         extraGroups = [ \"networkmanager\" \"wheel\" \"libvirtd\" ];\n         shell = pkgs.bash;\n         ignoreShellProgramCheck = true;\n         packages = with pkgs; [];\n       };\n    };\n    }\n</code></pre>\n<p>With <code>userborn</code>, you configure your users as you normally would declaratively\nwith NixOS with <code>users.users</code>, change <code>\"newuser\"</code> to your desired username.</p>\n<p>Explicitly setting <code>uid = 1000;</code> is a best practice for compatibility and\npredictability.</p>\n<hr />\n<p><strong>Only install, enable, and run what is needed</strong>: Disable or uninstall\nunnecessary software and services to minimize potential vulnerabilities. Take\nadvantage of NixOS’s easy package management and minimalism to keep your system\nlean and secure.</p>\n<p><strong>Avoid permanently installing temporary tools</strong>: Use tools like <code>nix-shell</code>,\n<code>comma</code>, <code>devShells</code> and <code>nix-direnv</code> to test or run software temporarily. This\nprevents clutter and reduces potential risks from unused software lingering on\nthe system.</p>\n<p><strong>Update regularly</strong>: Keep your system and software up to date to receive the\nlatest security patches. Delaying updates leaves known vulnerabilities open to\nexploitation.</p>\n<p><strong>Apply the Principle of Least Privilege</strong>: Never run tools or services as root\nunless absolutely necessary. Create dedicated users and groups with the minimum\nrequired permissions to limit potential damage if compromised.</p>\n<p><strong>Use strong passwords and passphrases</strong>: Aim for at least 14–16 characters by\ncombining several unrelated words, symbols, and numbers. For example:\n<code>sunset-CoffeeHorse$guitar!</code>. Strong passphrases are both memorable and secure.</p>\n<p><strong>Use a password manager and enable multi-factor authentication (MFA)</strong>: Manage\nunique, strong passwords effectively with a trusted manager and protect accounts\nwith MFA wherever possible for a second layer of defense.</p>\n<p><strong>Check logs regularly</strong>: Reviewing your system logs helps you spot unusual\nactivity, errors, or failed login attempts that could indicate a security\nproblem. NixOS uses <code>journald</code> by default, which makes this easy. For example,\nto see the logs for your current boot session:</p>\n<pre><code class=\"language-bash\">journalctl -b\n# for the previous session\njournalctl -b -1\n</code></pre>\n<p>After establishing some standard best practices and a hardened base, it’s time\nto dive deeper into system hardening, the process of adding layered safeguards\nthroughout your NixOS setup. This next section guides you through concrete steps\nand options for hardening critical areas of your system: from encryption and\nsecure boot to managing secrets, tightening kernel security, and leveraging\nplatform-specific tools.\n<a href=\"https://saylesss88.github.io/nix/hardening_NixOS.html\">Hardening NixOS</a></p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/installation/enc/lanzaboote.html",
      "url": "https://saylesss88.github.io/installation/enc/lanzaboote.html",
      "title": "Lanzaboote",
      "content_html": "<h1>Secure Boot with Lanzaboote</h1>\n<details>\n<summary> Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p>⚠️ <strong>Warning: This can easily brick your system</strong> ⚠️</p>\n<p>We will mainly follow the lanzaboote\n<a href=\"https://github.com/nix-community/lanzaboote/blob/master/docs/QUICK_START.md\">Quick Start Guide</a></p>\n<p>For Windows dual-booters and BitLocker users, you should export your BitLocker\nrecovery keys and confirm that they are correct. Refer to this\n<a href=\"https://support.microsoft.com/en-us/windows/find-your-bitlocker-recovery-key-6b71ad27-0b89-ea08-f143-056f5ab347d6\">Microsoft support article</a></p>\n<blockquote>\n<p>❗ NOTE: There are some serious limitations to this setup when used without\nencryption, I’d say it could stop the average person. But an experienced\nhacker could easily bypass this without encryption if they had access to your\ncomputer. For more protection look into TPM2 Hardware Requirements, and full\ndisk encryption.</p>\n</blockquote>\n<h2>Important Considerations</h2>\n<p>I found\n<a href=\"https://0pointer.net/blog/authenticated-boot-and-disk-encryption-on-linux.html\">This Article</a>\nfairly enlightening as far as the state of Authenticated Boot and Disk\nEncryption on Linux.</p>\n<ul>\n<li><a href=\"https://0pointer.net/blog/brave-new-trusted-boot-world.html\">Brave New Trusted Boot World</a></li>\n</ul>\n<p>Lanzaboote only secures the boot chain. The userspace remains unverified (i.e.,\nthe nix store, etc.), to verify userspace you need to implement additional\nintegrity checks. It’s common to rely to disk encryption to prevent tampering\nwith and keep the Nix store safe but it’s not always desirable. (i.e.,\nunattended boot)</p>\n<h2>Requirements</h2>\n<p>To be able to setup Secure Boot on your device, NixOS needs to be installed in\nUEFI mode and systemd-boot must be used as a boot loader. This means if you wish\nto install lanzaboote on a new machine, you need to follow the install\ninstruction for systemd-boot and then switch to lanzaboote after the first boot.</p>\n<p>Check these prerequisits with <code>bootctl status</code>, this is an example output:</p>\n<pre><code class=\"language-bash\">sudo bootctl status\nSystem:\n     Firmware: UEFI 2.70 (Lenovo 0.4720)\n  Secure Boot: disabled (disabled)\n TPM2 Support: yes\n Boot into FW: supported\n\nCurrent Boot Loader:\n      Product: systemd-boot 251.7\n...\n</code></pre>\n<p>The firmware <strong>must</strong> be <code>UEFI</code> and the current bootloader needs to be\n<code>systemd-boot</code>. If you check these boxes, you’re good to go.</p>\n<h2>Security Requirements</h2>\n<p>To provide any security your system needs to defend against an attacker turning\nUEFI Secure Boot off or being able to sign binaries with the keys we are going\nto generate.</p>\n<p>The easiest way to achieve this is to:</p>\n<ol>\n<li>\n<p>Enable a BIOS password for your system, this will prevent someone from just\nshutting off secure boot.</p>\n</li>\n<li>\n<p>Use full disk encryption.</p>\n</li>\n</ol>\n<h2>Preparation</h2>\n<p><strong>Finding the UEFI System Partition (ESP)</strong></p>\n<p>The UEFI boot process revolves around the ESP, the (U)EFI System Partition. This\npartition is conventionally mounted at <code>/boot</code> on NixOS.</p>\n<p>Verify this with the command <code>sudo bootctl status</code>. Look for <code>ESP:</code></p>\n<p><strong>Creating Your Keys</strong></p>\n<p>First you’ll need to install <code>sbctl</code> which is available in <code>Nixpkgs</code>:</p>\n<pre><code class=\"language-nix\"># configuration.nix or equivalent\nenvironment.systemPackages = [ pkgs.sbctl ];\n</code></pre>\n<p>Create the keys:</p>\n<pre><code class=\"language-bash\">$ sudo sbctl create-keys\n[sudo] password for julian:\nCreated Owner UUID 8ec4b2c3-dc7f-4362-b9a3-0cc17e5a34cd\nCreating secure boot keys...✓\nSecure boot keys created!\n</code></pre>\n<p>If you already have keys in <code>/etc/secureboot</code> migrate these to <code>/var/lib/sbctl</code>:</p>\n<pre><code class=\"language-bash\">sbctl setup --migrate\n</code></pre>\n<h2>Configuring Lanzaboote With Flakes</h2>\n<p>Shown all in <code>flake.nix</code> for brevity. Can easily be split up into a <code>boot.nix</code>,\netc:</p>\n<pre><code class=\"language-nix\">{\n  description = \"A SecureBoot-enabled NixOS configurations\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n\n    lanzaboote = {\n      url = \"github:nix-community/lanzaboote/v0.4.2\";\n\n      # Optional but recommended to limit the size of your system closure.\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n\n  outputs = { self, nixpkgs, lanzaboote, ...}: {\n    nixosConfigurations = {\n      yourHost = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n\n        modules = [\n          # This is not a complete NixOS configuration and you need to reference\n          # your normal configuration here.\n\n          lanzaboote.nixosModules.lanzaboote\n\n          ({ pkgs, lib, ... }: {\n\n            environment.systemPackages = [\n              # For debugging and troubleshooting Secure Boot.\n              pkgs.sbctl\n            ];\n\n            # Lanzaboote currently replaces the systemd-boot module.\n            # This setting is usually set to true in configuration.nix\n            # generated at installation time. So we force it to false\n            # for now.\n            boot.loader.systemd-boot.enable = lib.mkForce false;\n\n            boot.lanzaboote = {\n              enable = true;\n              pkiBundle = \"/var/lib/sbctl\";\n            };\n          })\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<p><strong>Build it</strong></p>\n<pre><code class=\"language-bash\">sudo nixos-rebuild switch --flake /path/to/flake\n</code></pre>\n<h3>Ensure Your Machine is Ready for Secure Boot enforcement</h3>\n<pre><code class=\"language-bash\">$ sudo sbctl verify\nVerifying file database and EFI images in /boot...\n✓ /boot/EFI/BOOT/BOOTX64.EFI is signed\n✓ /boot/EFI/Linux/nixos-generation-355.efi is signed\n✓ /boot/EFI/Linux/nixos-generation-356.efi is signed\n✗ /boot/EFI/nixos/0n01vj3mq06pc31i2yhxndvhv4kwl2vp-linux-6.1.3-bzImage.efi is not signed\n✓ /boot/EFI/systemd/systemd-bootx64.efi is signed\n</code></pre>\n<h3>Enabling Secure Boot and Entering Setup Mode</h3>\n<p>This is where things can get tricky because UEFI/BIOS are widely different and\nuse different conventions.</p>\n<p>You can see your BIOS from the output of <code>bootctl status</code>:</p>\n<pre><code class=\"language-bash\">sudo bootctl status\ndoas (jr@magic) password:\nSystem:\n      Firmware: UEFI 2.70 (American Megatrends)\n</code></pre>\n<p>My UEFI is an American Megatrends, find yours and look up which key you have to\nhit to enter the BIOS on reboot, mine is the delete key. So I reboot and\nrepeatedly hit delete until it brings up the BIOS settings.</p>\n<p>The lanzaboote guide shows a few systems and how to enter setup mode for them.</p>\n<p>For a ThinkPad the steps are:</p>\n<ol>\n<li>\n<p>Select the “Security” tab.</p>\n</li>\n<li>\n<p>Select the “Secure Boot” entry.</p>\n</li>\n<li>\n<p>Set “Secure Boot” to enabled.</p>\n</li>\n<li>\n<p>Select “Reset to Setup Mode”.</p>\n</li>\n</ol>\n<hr />\n<p>For my system, it would allow me to do the above steps but when I saved and\nexited I got a red screen then blue screen and it said No Valid Keys or\nsomething like that and eventually brought me to the MOK Manager where you can\nmanually register keys, this is NOT what you want to do.</p>\n<p>Even after this mistake I was able to re-enable secure boot and get back into\nthe system.</p>\n<p>After some tinkering, I found that I was able to enter “custom mode” without\nenabling secure boot, which in turn allowed me to select the “Reset to Setup\nMode”</p>\n<p>It asks if you are sure you want to erase all of the variables to enter setup\nmode? Hit “Yes”. Then it asks if you want to exit without saving, we want to\nsave our changes so hit “No” do not exit without saving.</p>\n<p>After this you should see all No Keys entries.</p>\n<p>Finally, Hit the setting to save and exit, some BIOS list an F4 or F9 keybind\nthat saves and exits.</p>\n<blockquote>\n<p>❗: For my system, choosing “save and reboot” would not work for some reason,\nI had to choose “save and exit”.</p>\n</blockquote>\n<p>After hitting “save and exit”, the system boots into NixOS like normal but you\nare in setup mode if everything worked correctly.</p>\n<p>Open a terminal and type:</p>\n<pre><code class=\"language-bash\">sudo sbctl enroll-keys --microsoft\nEnrolling keys to EFI variables...\nWith vendor keys from microsoft...✓\nEnrolled keys to the EFI variables!\n</code></pre>\n<blockquote>\n<p>⚠️ If you used <code>--microsoft</code> while enrolling the keys, you might want to check\nthat the Secure Boot Forbidden Signature Database (dbx) is not empty. A quick\nand dirty way is by checking the file size of\n<code>/sys/firmware/efi/efivars/dbx-\\*</code>. Keeping an up to date dbx reduces Secure\nBoot bypasses, see for example:\n<a href=\"https://uefi.org/sites/default/files/resources/dbx_release_info.pdf\">https://uefi.org/sites/default/files/resources/dbx_release_info.pdf</a></p>\n</blockquote>\n<p>I then Rebooted into BIOS and enabled secure boot, saved and exited. This loads\nNixOS as if you just rebooted.</p>\n<p>And finally check the output of <code>sbctl status</code>:</p>\n<pre><code class=\"language-bash\">sudo sbctl status\nSystem:\n      Firmware: UEFI 2.70 (American Megatrends)\n Firmware Arch: x64\n   Secure Boot: enabled (user)\n  TPM2 Support: yes\n  Measured UKI: yes\n  Boot into FW: supported\n</code></pre>\n<p>We can see the <code>Secure Boot: enabled (user)</code></p>\n<h2>What Lanzaboote (Secure Boot) Actually Secures on NixOS and Limitations</h2>\n<p>As mentioned earlier, this provides some basic protection that may be good\nenough for your desktop in your bedroom but there are some serious limitations.\nI want to be clear that this may stop an average person but an advanced threat\nactor with resources could still fairly easily get in.</p>\n<p>Secure Boot (with Lanzaboote or any other tool) on NixOS primarily protects the\nboot chain—the bootloader, kernel, and initrd—by ensuring only signed, trusted\nbinaries are executed at boot. This is a real and valuable security improvement,\nespecially for defending against “evil maid” attacks (where someone with\nphysical access tampers with your bootloader or kernel) and for preventing many\nforms of persistent malware.</p>\n<p>Here are some of the caveats:</p>\n<ol>\n<li>\n<p>Userspace Remains Unverified</p>\n<p>Once the kernel and initrd have booted, NixOS (by default) does not\ncryptographically verify the integrity of the rest of userspace (the programs\nand libraries in the Nix store, your configs, etc.).</p>\n<p>This means an attacker who can modify userspace (e.g., by gaining root\naccess) can potentially install persistent malware, even if your boot chain\nis protected</p>\n<p>.</p>\n</li>\n<li>\n<p>Kernel Lockdown Is Not Enabled</p>\n<p>The Linux kernel’s [lockdown mode]</p>\n<p>is designed to prevent even root from tampering with the kernel at runtime\n(e.g., by loading unsigned modules, using kexec, or accessing /dev/mem).</p>\n<p>NixOS does not enable kernel lockdown by default, and enabling it is\nnon-trivial, especially given how the Nix store works (modules and kernels\nare built dynamically and not always signed at install time).</p>\n<p>Without lockdown, a root user (or malware with root) can still compromise the\nkernel after boot.</p>\n</li>\n<li>\n<p>Stage 2 Verification Is Lacking</p>\n<p>Some distributions (like Fedora Silverblue or systems using dm-verity)\ncryptographically verify the entire userspace at boot, making it immutable\nand much harder to tamper with. This is not the default on NixOS, though\nthere are experimental or appliance-focused solutions</p>\n<p>.</p>\n</li>\n<li>\n<p>Disk Encryption Complements Secure Boot</p>\n<p>Full disk encryption (e.g., LUKS) is strongly recommended alongside Secure\nBoot. Encryption protects your data at rest and ensures that even if someone\nbypasses Secure Boot, they cannot read or modify your files without your\npassphrase</p>\n</li>\n</ol>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Working_with_Nixpkgs_Locally_10.html",
      "url": "https://saylesss88.github.io/Working_with_Nixpkgs_Locally_10.html",
      "title": "Local Nixpkgs",
      "content_html": "<h1>Chapter 10</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/server_rack.cleaned.png\" alt=\"server_rack\" /></p>\n<!-- ![gruv18](images/gruv18.png) -->\n<h2>Working with Nixpkgs Locally: Benefits and Best Practices</h2>\n<ul>\n<li>\n<p>Nixpkgs, the package repository for NixOS, is a powerful resource for building\nand customizing software.</p>\n</li>\n<li>\n<p>Working with a local copy enhances development, debugging, and contribution\nworkflows.</p>\n</li>\n<li>\n<p>This post covers setting up a local Nixpkgs repository, searching for\ndependencies, and leveraging its advantages, incorporating tips from the Nix\ncommunity.</p>\n</li>\n</ul>\n<h1>I. Why Work with Nixpkgs Locally?</h1>\n<ul>\n<li>\n<p>A local Nixpkgs repository offers significant advantages for Nix developers:</p>\n<h2>A. Faster Development Cycle</h2>\n<ul>\n<li>\n<p>Local searches for packages and dependencies are significantly quicker than\nquerying remote repositories or channels.</p>\n</li>\n<li>\n<p>This speedup is crucial for efficient debugging and rapid prototyping of Nix\nexpressions.</p>\n</li>\n</ul>\n<h2>B. Enhanced Version Control</h2>\n<ul>\n<li>\n<p>By pinning your local repository to specific commits or branches (e.g.,\n<code>nixos-unstable</code>), you ensure build reproducibility.</p>\n</li>\n<li>\n<p>This prevents unexpected issues arising from upstream changes in Nixpkgs.</p>\n</li>\n</ul>\n<h2>C. Flexible Debugging Capabilities</h2>\n<ul>\n<li>\n<p>You can directly test and modify package derivations within your local copy.</p>\n</li>\n<li>\n<p>This allows for quick fixes to issues like missing dependencies without\nwaiting for upstream updates or releases.</p>\n</li>\n</ul>\n<h2>D. Streamlined Contribution Workflow</h2>\n<ul>\n<li>\n<p>Developing and testing new packages or patches locally is essential before\nsubmitting them as pull requests to Nixpkgs.</p>\n</li>\n<li>\n<p>A local setup provides an isolated environment for experimentation.</p>\n</li>\n</ul>\n<h2>E. Up-to-Date Documentation Source</h2>\n<ul>\n<li>The source code and comments within the Nixpkgs repository often contain the\nmost current information about packages.</li>\n<li>This can sometimes be more up-to-date than official, external documentation.</li>\n</ul>\n<h2>F. Optimized Storage and Performance</h2>\n<ul>\n<li>Employing efficient cloning strategies (e.g., shallow clones) and avoiding\nunnecessary practices (like directly using Nixpkgs as a flake for local\ndevelopment) minimizes disk usage and build times.</li>\n</ul>\n</li>\n</ul>\n<h1>II. Flake vs. Non-Flake Syntax for Local Nixpkgs</h1>\n<ul>\n<li>\n<p>When working with Nixpkgs locally, the choice between Flake and non-Flake\nsyntax has implications for performance and storage:</p>\n<h2>A. Flake Syntax (<code>nix build .#&lt;package&gt;</code>)</h2>\n<ul>\n<li>\n<p>Treats the current directory as a flake, requiring evaluation of\n<code>flake.nix</code>.</p>\n</li>\n<li>\n<p>For local Nixpkgs, this evaluates the flake definition in the repository\nroot.</p>\n</li>\n<li>\n<p><strong>Performance and Storage Overhead:</strong> Flakes copy the entire working\ndirectory (including Git history if present) to <code>/nix/store</code> for evaluation.\nThis can be slow and storage-intensive for large repositories like Nixpkgs.</p>\n</li>\n</ul>\n<h2>B. Non-Flake Syntax (<code>nix-build -f . &lt;package&gt;</code> or <code>nix build -f . &lt;package&gt;</code>)</h2>\n<ul>\n<li>\n<p><code>-f .</code> specifies the Nix expression (e.g., <code>default.nix</code> or a specific file)\nin the current directory.</p>\n</li>\n<li>\n<p><strong>Efficiency:</strong> Evaluates the Nix expression directly <em>without</em> copying the\nentire worktree to <code>/nix/store</code>. This is significantly faster and more\nstorage-efficient for local development on large repositories.</p>\n</li>\n</ul>\n</li>\n</ul>\n<h2>III. Setting Up a Local Nixpkgs Repository Efficiently</h2>\n<ul>\n<li>Cloning Nixpkgs requires careful consideration due to its size.</li>\n</ul>\n<h2>A.a Initial Clone: Shallow Cloning</h2>\n<p>It is common to place your local clone in the <code>/src</code> directory:</p>\n<pre><code class=\"language-bash\">mkdir src &amp;&amp; cd src\n</code></pre>\n<blockquote>\n<p>❗ Warning, A shallow clone (<code>--depth 1</code>) is not recommended for general\ndevelopment or contributing changes back to Nixpkgs via pull requests. It’s\nprimarily suitable for:</p>\n<ul>\n<li>Quick checks or builds: If you only need to verify a package’s current state\nor build a specific version without needing historical context.</li>\n<li>CI/CD environments: Where disk space and clone time are critical, and only\nthe latest commit is needed for automated tests or builds.</li>\n</ul>\n</blockquote>\n<p>With that said, to avoid downloading the entire history, perform a shallow\nclone:</p>\n<pre><code class=\"language-bash\">git clone [https://github.com/NixOS/nixpkgs](https://github.com/NixOS/nixpkgs) --depth 1\ncd nixpkgs\n</code></pre>\n<h2>A.b A few Examples exploring Nixpkgs</h2>\n<p>While in the <code>nixpkgs</code> directory, you can check the version of a package:</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval -A openssl.version\n\"3.4.1\"\n</code></pre>\n<p>Or to directly edit the file you can use <code>nix edit</code>:</p>\n<pre><code class=\"language-bash\">nix edit nixpkgs#openssl\n</code></pre>\n<p>It uses the nix registry and <code>openssl.meta.position</code> to locate the file.</p>\n<pre><code class=\"language-bash\">man nix3 registry\n</code></pre>\n<p>The above command will open the <code>openssl/default.nix</code> in your <code>$EDITOR</code>.</p>\n<hr />\n<h2>A.1 Full Fork and Clone of Nixpkgs</h2>\n<p>If you want to contribute to Nixpkgs, you need to set up a local version\nfollowing the\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md\">Contributing guide</a></p>\n<p>You’ll need to, this is directly from the <code>Contributing.md</code>:</p>\n<ol>\n<li>\n<p><a href=\"https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#forking-a-repository\">Fork</a>\nthe <a href=\"https://github.com/nixos/nixpkgs/\">Nixpkgs repository</a></p>\n</li>\n<li>\n<p><a href=\"https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#cloning-your-forked-repository\">Clone the forked repo</a>\ninto a local <code>nixpkgs</code> directory.</p>\n</li>\n</ol>\n<pre><code class=\"language-bash\">git clone git@github.com:your-user/nixpkgs.git\n</code></pre>\n<ol start=\"3\">\n<li><a href=\"https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#configuring-git-to-sync-your-fork-with-the-upstream-repository\">Configure the upstream Nixpkgs repo</a></li>\n</ol>\n<pre><code class=\"language-bash\">git remote add upstream git@github.com:NixOS/nixpkgs.git\n# Check them out\ngit remove -v\n</code></pre>\n<h3>The Three Master Branches</h3>\n<ol>\n<li><strong>Upstream Master</strong> (upstream/master):</li>\n</ol>\n<ul>\n<li>\n<p><strong>Where it is</strong>: On the official NixOS servers.</p>\n</li>\n<li>\n<p><strong>Role</strong>: The absolute source of truth. Thousands of people are pushing to\nthis daily.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li><strong>Local Master</strong> (master):</li>\n</ol>\n<ul>\n<li>\n<p><strong>Where it is</strong>: On your physical computer.</p>\n</li>\n<li>\n<p><strong>Role</strong>: Your working copy. This is the only one you can actually “rebase” or\n“commit” to directly.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li><strong>Origin Master</strong> (origin/master):</li>\n</ol>\n<ul>\n<li>\n<p><strong>Where it is</strong>: On your GitHub fork (github.com/your-user/nixpkgs).</p>\n</li>\n<li>\n<p><strong>Role</strong>: A personal backup and a place to host your code so you can open Pull\nRequests.</p>\n</li>\n</ul>\n<h3>Create a branch</h3>\n<p>In the nixpkgs ecosystem, the “cleanest” way to work is to <strong>never</strong> add your\nown commits to your local master.</p>\n<ul>\n<li>\n<p>Keep your <code>master</code> as a “pure mirror” of <code>upstream/master</code>.</p>\n</li>\n<li>\n<p>Whenever you want to fix a package or add something new, create a feature\nbranch:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">git checkout -b fix-my-package master\n</code></pre>\n<p>This way, syncing is as simple as a reset:</p>\n<pre><code class=\"language-bash\"># Total reset of your local master to match the official one\ngit fetch upstream\ngit checkout master\ngit reset --hard upstream/master\ngit push origin master --force\n</code></pre>\n<h3>Pushing a PR</h3>\n<p>When you’re ready to push changes you push to <code>origin/feature-branch</code>, visit\nyour fork on github.com and submit the PR.</p>\n<h2>B. Managing Branches with Worktrees</h2>\n<ul>\n<li>\n<p>Use Git worktrees to manage different branches efficiently:</p>\n<pre><code class=\"language-bash\">git fetch --all --prune --depth=1\ngit worktree add -b nixos-unstable nixos-unstable # For just unstable\n</code></pre>\n</li>\n<li>\n<p><strong>Explanation of <code>git worktree</code>:</strong> Allows multiple working directories\nattached to the same <code>.git</code> directory, sharing history and objects but with\nseparate checked-out files.</p>\n</li>\n<li>\n<p><code>git worktree add</code>: Creates a new working directory for the specified branch\n(<code>nixos-unstable</code> in this case).</p>\n</li>\n</ul>\n<h1>IV. Debugging Missing Dependencies: A Practical Example</h1>\n<details>\n<summary> Click to see icat Example </summary>\n<ul>\n<li>Let’s say you’re trying to build <code>icat</code> locally and encounter a missing\ndependency error:</li>\n</ul>\n<pre><code class=\"language-nix\">nix-build -A icat\n# ... (Error log showing \"fatal error: X11/Xlib.h: No such file or directory\")\n</code></pre>\n<ul>\n<li>The error <code>fatal error: X11/Xlib.h: No such file or directory</code> indicates a\nmissing X11 dependency.</li>\n</ul>\n<h2>A. Online Search with <code>search.nixos.org</code></h2>\n<ul>\n<li>The Nixpkgs package search website\n(<a href=\"https://search.nixos.org/packages\">https://search.nixos.org/packages</a>) is a\nvaluable first step.</li>\n<li>However, broad terms like “x11” can yield many irrelevant results.</li>\n<li><strong>Tip:</strong> Utilize the left sidebar to filter by package sets (e.g., “xorg”).</li>\n</ul>\n<h2>B. Local Source Code Search with <code>rg</code> (ripgrep)</h2>\n<ul>\n<li>\n<p>Familiarity with searching the Nixpkgs source code is crucial for finding\ndependencies.</p>\n</li>\n<li>\n<p>Navigate to your local <code>nixpkgs/</code> directory and use <code>rg</code>:</p>\n<pre><code class=\"language-bash\">rg \"x11 =\" pkgs # Case-sensitive search\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code>pkgs/tools/X11/primus/default.nix\n21:  primus = if useNvidia then primusLib_ else primusLib_.override { nvidia_x11 = null; };\n22:  primus_i686 = if useNvidia then primusLib_i686_ else primusLib_i686_.override { nvidia_x11 = null; };\n\npkgs/applications/graphics/imv/default.nix\n38:    x11 = [ libGLU xorg.libxcb xorg.libX11 ];\n</code></pre>\n</li>\n<li>\n<p>Refining the search (case-insensitive):</p>\n<pre><code class=\"language-bash\">rg -i \"libx11 =\" pkgs\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code># ... (Output showing \"xorg.libX11\")\n</code></pre>\n</li>\n<li>\n<p>The key result is <code>xorg.libX11</code>, which is likely the missing dependency.</p>\n</li>\n</ul>\n </details>\n<h1>V. Local Derivation Search with <code>nix-locate</code></h1>\n<details>\n<summary> Click to Expand nix-locate command Example</summary>\n<ul>\n<li>\n<p><code>nix-locate</code> (from the <code>nix-index</code> package) allows searching for derivations\non the command line.</p>\n<blockquote>\n<p><strong>Note:</strong> Install <code>nix-index</code> and run <code>nix-index</code> to create the initial\nindex.</p>\n</blockquote>\n<pre><code class=\"language-bash\">nix-locate libx11\n# ... (Output showing paths related to libx11)\n</code></pre>\n</li>\n<li>\n<p>Combining online and local search tools (<code>search.nixos.org</code>, <code>rg</code>,\n<code>nix-locate</code>) provides a comprehensive approach to finding dependencies.</p>\n</li>\n</ul>\n</details>\n<h1>VI. Key Benefits of Working with Nixpkgs Locally (Recap)</h1>\n<ul>\n<li>\n<p><strong>Speed:</strong> Faster searches and builds compared to remote operations.</p>\n</li>\n<li>\n<p><strong>Control:</strong> Full control over the Nixpkgs version.</p>\n</li>\n<li>\n<p><strong>Up-to-Date Information:</strong> Repository source often has the latest details.</p>\n</li>\n</ul>\n<h1>VII. Best Practices and Tips from the Community</h1>\n<details>\n<summary> ✔️ Click To Expand Best Practices and Tips from the community</summary>\n<ul>\n<li>\n<p><strong>Rebasing over Merging:</strong> Never merge upstream changes into your local\nbranch. Always rebase your branch onto the upstream (e.g., <code>master</code> or\n<code>nixos-unstable</code>) to avoid accidental large-scale pings in pull requests (Tip\nfrom <code>soulsssx3</code> on Reddit).</p>\n</li>\n<li>\n<p><strong>Tip from <code>ElvishJErrico</code>:</strong> Avoid using Nixpkgs directly as a flake for\nlocal development due to slow copying to <code>/nix/store</code> and performance issues\nwith garbage collection on large numbers of small files. Use\n<code>nix build -f . &lt;package&gt;</code> instead of <code>nix build .#&lt;package&gt;</code>.</p>\n</li>\n<li>\n<p><strong>Edge Cases for Flake Syntax:</strong> Flake syntax might be necessary in specific\nscenarios, such as NixOS installer tests where copying the Git history should\nbe avoided.</p>\n</li>\n<li>\n<p><strong>Base Changes on <code>nixos-unstable</code>:</strong> For better binary cache hits, base your\nchanges on the <code>nixos-unstable</code> branch instead of <code>master</code>. Consider the\nmerge-base for staging branches as well.</p>\n</li>\n<li>\n<p><strong>Consider <code>jujutsu</code>:</strong> Explore <a href=\"https://github.com/jj-vcs/jj\">jj-vcs</a>, a\nGit-compatible alternative that can offer a more intuitive workflow,\nespecially for large monorepos like Nixpkgs. While it has a learning curve, it\ncan significantly improve parallel work and branch management.</p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/vcs/jujutsu.html\">Intro-To-Jujutsu</a></p>\n</li>\n</ul>\n</details>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nixpkgs/fork_clone_contribute.html",
      "url": "https://saylesss88.github.io/nixpkgs/fork_clone_contribute.html",
      "title": "Fork, Clone, Contribute",
      "content_html": "<h1>Fork, Clone, Contribute</h1>\n<ul>\n<li>\n<p>In the <a href=\"https://github.com/NixOS/nixpkgs\">Nixpkgs</a> Repository.</p>\n</li>\n<li>\n<p>Click Fork, then Create a new Fork.</p>\n</li>\n<li>\n<p>Uncheck the box “Only fork the <code>master</code> branch”, for development we will need\nmore branches.</p>\n<ul>\n<li>If you only fork master, you won’t have the <code>nixos-XX.YY</code> release branches\navailable on your fork when you later try to create a PR against them, or\nwhen you want to create a feature branch from them on your fork.</li>\n</ul>\n</li>\n<li>\n<p>Click <code>&lt;&gt; Code</code> and Clone the Repo. <code>sayls8</code> is the name of my GitHub, yours\nwill obviously be different.</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">git clone git@github.com:sayls8/nixpkgs.git\n</code></pre>\n<p>Figure out the branch that should be used for this change by going through\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#branch-conventions\">this section</a></p>\n<p>When in doubt use <code>master</code>, that’s where most changes should go. This can be\nchanged later by\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#rebasing-between-branches-ie-from-master-to-staging\">rebasing</a></p>\n<p>Add <a href=\"https://github.com/NixOS/nixpkgs\">Nixpkgs</a> as your upstream:</p>\n<pre><code class=\"language-bash\">cd nixpkgs\n\ngit remote add upstream https://github.com/NixOS/nixpkgs.git\n# Make sure you have the latest changes from upstream Nixpkgs\ngit fetch upstream\n# Show currently configured remote repository\ngit remote -v\norigin  git@github.com:sayls8/nixpkgs.git (fetch)\norigin  git@github.com:sayls8/nixpkgs.git (push)\nupstream        https://github.com/NixOS/nixpkgs.git (fetch)\nupstream        https://github.com/NixOS/nixpkgs.git (push)\n</code></pre>\n<p><strong>Understanding Your Remotes</strong></p>\n<p>This output confirms that:</p>\n<ul>\n<li>\n<p><code>origin</code> is your personal fork on GitHub (<code>sayls8/nixpkgs.git</code>). When you\n<code>git push origin ...</code>, your changes go here.</p>\n</li>\n<li>\n<p><code>upstream</code> is the official Nixpkgs repository (<code>NixOS/nixpkgs.git</code>). When you\n<code>git fetch upstream</code>, you’re getting the latest updates from the main project.</p>\n</li>\n</ul>\n<p>This setup ensures you can easily pull updates from the original project and\npush your contributions to your own fork.</p>\n<pre><code class=\"language-bash\"># Shows a ton of remote branches\ngit branch -r | grep upstream\n# Narrow it down\ngit branch -r | grep upstream | grep nixos-\n</code></pre>\n<p>Next Steps for Contributing</p>\n<ol>\n<li>Ensure <code>master</code> is up to date with <code>upstream</code></li>\n</ol>\n<pre><code class=\"language-bash\">git checkout master\ngit pull upstream master\ngit push origin master\n</code></pre>\n<ul>\n<li>\n<p><code>git pull upstream master</code> is equivalent to running <code>git fetch upstream</code>\nfollowed by <code>git merge upstream/master</code> into your current branch (<code>master</code>).</p>\n</li>\n<li>\n<p><code>git push origin master</code> updates your forks remote with the fetched changes.</p>\n</li>\n</ul>\n<p>This keeps your fork in sync to avoid conflicts.</p>\n<p>If targeting another branch, replace <code>master</code> with <code>nixos-24.11</code> for example.</p>\n<ol start=\"2\">\n<li>Create a Feature Branch</li>\n</ol>\n<pre><code class=\"language-bash\">git checkout master\ngit checkout -b my-feature-branch # name should represent the feature\n</code></pre>\n<ol start=\"3\">\n<li>Make and Test Changes</li>\n</ol>\n<p><a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/README.md#conventions\">Packaging Conventions</a></p>\n<p><strong>New package</strong>: Add to\n<code>pkgs/by-name/&lt;first-two-letters&gt;/&lt;package-name&gt;/default.nix</code>.</p>\n<p><strong>Example structure</strong>:</p>\n<pre><code class=\"language-nix\">{ lib, stdenv, fetchFromGitHub }: stdenv.mkDerivation {\npname = \"xyz\"; version = \"1.2.3\"; src = fetchFromGitHub { ... }; ... }\n</code></pre>\n<p><strong>Update package</strong>: Edit version and <code>sha256</code> in the package’s <code>default.nix</code>.\nUse <code>nix-prefetch-url</code> to update hashes:</p>\n<pre><code class=\"language-bash\">nix-prefetch-url &lt;source-url&gt;\n</code></pre>\n<p><strong>Fix a bug</strong>: Modify files in <code>pkgs/</code>, <code>nixos/modules/</code>, or elsewhere.</p>\n<p><strong>Test locally</strong>:</p>\n<p>Build:</p>\n<pre><code class=\"language-bash\">nix-build -A &lt;package-name&gt;\n</code></pre>\n<p><strong>Test in a shell</strong>:</p>\n<pre><code class=\"language-bash\">nix-shell -p &lt;package-name&gt;\n</code></pre>\n<p>For NixOS modules:</p>\n<pre><code class=\"language-bash\">nixos-rebuild test\n</code></pre>\n<p>Follow the Nixpkgs Contributing Guide.</p>\n<ol start=\"4\">\n<li><strong>Commit and Push</strong></li>\n</ol>\n<p>Commit with a clear message, make sure to follow\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#commit-conventions\">commit conventions</a>:</p>\n<p><strong>Commit Conventions</strong></p>\n<ul>\n<li>\n<p>Create a commit for each logical unit.</p>\n</li>\n<li>\n<p>Check for unnecessary whitespace with <code>git diff --check</code> before committing.</p>\n</li>\n<li>\n<p>If you have commits <code>pkg-name: oh, forgot to insert whitespace</code>: squash\ncommits in this case. Use <code>git rebase -i</code>. See\n<a href=\"https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#_squashing\">Squashing Commits</a>\nfor additional information.</p>\n</li>\n<li>\n<p>For consistency, there should not be a period at the end of the commit\nmessage’s summary line (the first line of the commit message).</p>\n</li>\n<li>\n<p>When adding yourself as maintainer in the same pull request, make a separate\ncommit with the message maintainers: <code>add &lt;handle&gt;</code>. Add the commit before\nthose making changes to the package or module. See\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/maintainers/README.md\">Nixpkgs Maintainers</a>\nfor details.</p>\n</li>\n</ul>\n<p>Format the commit messages in the following way:</p>\n<pre><code class=\"language-bash\">(pkg-name): (from -&gt; to | init at version | refactor | etc)\n\n(Motivation for change. Link to release notes. Additional information.)\n</code></pre>\n<p>a) For example, for the <code>airshipper</code> package:</p>\n<pre><code class=\"language-bash\">git add pkgs/by-name/ai/airshipper/\ngit commit -m \"airshipper: init at 0.1.0\"\n\nAdds the airshipper tool for managing game assets.\nUpstream homepage: https://github.com/someuser/airshipper\"\n</code></pre>\n<p>b) Updating <code>airshipper</code> to a new version</p>\n<pre><code class=\"language-bash\">git add pkgs/by-name/ai/airshipper/\ngit commit -m \"airshipper: 0.1.0 -&gt; 0.2.0\n\nUpdated airshipper to version 0.2.0. This release includes:\n- Improved asset fetching logic\n- Bug fixes for network errors\n\nRelease notes: https://github.com/someuser/airshipper/releases/tag/v0.2.0\"\n</code></pre>\n<p>c) Fixing a bug in <code>airshipper</code>’s package definition</p>\n<pre><code class=\"language-bash\">git add pkgs/by-name/ai/airshipper/\ngit commit -m \"airshipper: fix: build with latest glibc\n\nResolved build failures on unstable channel due to changes in glibc.\nPatched source to use updated API calls.\n\"\n</code></pre>\n<p>Examples:</p>\n<ul>\n<li>\n<p><code>nginx: init at 2.0.1</code></p>\n</li>\n<li>\n<p><code>firefox: 122.0 -&gt; 123.0</code></p>\n</li>\n<li>\n<p><code>vim: fix build with gcc13</code></p>\n</li>\n</ul>\n<p>Push:</p>\n<pre><code class=\"language-bash\">git push origin my-feature-branch\n</code></pre>\n<p>When you push your feature branch, it will output a link that you can follow to\ncomplete the PR on GitHub.</p>\n<p>If you have the <code>gh-cli</code> set up you can also do this from the command line:</p>\n<pre><code class=\"language-bash\">gh pr create --repo NixOS/nixpkgs --base master --head sayls8:feat/my-package\n</code></pre>\n<ol start=\"5\">\n<li>Create a Pull Request</li>\n</ol>\n<p>Go to <a href=\"https://github.com/sayls8/nixpkgs\">https://github.com/sayls8/nixpkgs</a>. (your fork) Click the PR prompt for\nmy-feature-branch. Set the base branch to <code>NixOS/nixpkgs:master</code> (or\n<code>nixos-24.11</code>). Write a PR description: Purpose of the change. Related issues\n(e.g., Fixes #1234). Testing steps (e.g., <code>nix-build -A &lt;package-name&gt;</code>). Submit\nand respond to feedback.</p>\n<ol start=\"6\">\n<li>Handle Updates</li>\n</ol>\n<p>For reviewer feedback or upstream changes:</p>\n<p>Edit, commit, and push:</p>\n<pre><code class=\"language-bash\">git add . git commit -m \"&lt;package-name&gt;: address feedback\" git push origin my-feature-branch\n</code></pre>\n<p>Rebase if needed:</p>\n<pre><code class=\"language-bash\">git fetch upstream\ngit rebase upstream/master  # or upstream/nixos-24.11\ngit push origin my-feature-branch --force\n</code></pre>\n<ol start=\"7\">\n<li>Cleanup</li>\n</ol>\n<p>After PR merge:</p>\n<p>Delete branch:</p>\n<pre><code class=\"language-bash\">git push origin --delete my-feature-branch\n</code></pre>\n<p>Sync master:</p>\n<pre><code class=\"language-bash\">git checkout master\ngit pull upstream master\ngit push origin master\n</code></pre>\n<p>Addressing the Many Branches</p>\n<ul>\n<li>\n<p>No need to manage all branches: The <code>nixos-branches</code> are just metadata from\nupstream. You only check out the one you need (e.g., <code>master</code> or\n<code>nixos-24.11</code>).</p>\n</li>\n<li>\n<p>Focus on relevant branches: The filter (<code>grep nixos-</code>) shows the key release\nbranches. Ignore -small branches and older releases unless specifically\nrequired. Confirm latest stable: If you’re targeting a stable branch,\n<code>nixos-24.11</code> is likely the latest (or <code>nixos-25.05</code> if it’s active). Verify\nvia NixOS status.</p>\n</li>\n</ul>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nixpkgs/local_package.html",
      "url": "https://saylesss88.github.io/nixpkgs/local_package.html",
      "title": "Local Nixpkgs",
      "content_html": "<details>\n<summary> ✔️ Table of Contents</summary>\n<!-- toc -->\n</details>\n<h1>Creating and Building a Local Package within a Nixpkgs Clone</h1>\n<p>This chapter demonstrates the fundamental pattern for creating a package. Every\npackage recipe is a file that declares a function. This function takes the\npackages dependencies as argument.</p>\n<p>In this example we’ll make a simple package with <code>coreutils</code> and build it.\nDemonstrating the process of building and testing a local package.</p>\n<p>This chapter will assume you have already have a cloned fork of Nixpkgs. I\nchoose to clone mine to the <code>~/src/</code> directory.</p>\n<p>You can check out the <code>nixpkgs/pkgs/README.md</code>\n<a href=\"https://github.com/NixOS/nixpkgs/tree/master/pkgs\">Here</a></p>\n<p>The Nixpkgs Contributing Guide can be found\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md\">Here</a></p>\n<h2>Create your Package directory and a <code>default.nix</code></h2>\n<p>For this example, we’ll create a package called <code>testPackage</code> and will place it\nin the <code>nixpkgs/pkgs/misc</code> directory.</p>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs/pkgs/misc\nmkdir testPackage &amp;&amp; cd testPackage\nhx default.nix\n</code></pre>\n<pre><code class=\"language-nix\"># default.nix\n{\n  runCommand,\n  coreutils,\n}:\nrunCommand \"testPackage\" {\n  nativeBuildInputs = [\n    coreutils\n  ];\n} ''\n\n  echo 'This is a Test' &gt; $out\n''\n</code></pre>\n<p>Now we need to add our <code>testPackage</code> to <code>all-packages.nix</code></p>\n<pre><code class=\"language-bash\">cd pkgs/top-level\nhx all-packages.nix\n</code></pre>\n<p><code>all-packages.nix</code> is a centralized module that defines all available package\nexpressions.</p>\n<p>We’ll add our package in the list alphabetically:</p>\n<pre><code class=\"language-nix\"># all-packages.nix\n# `/msc` # editor search inside file\n# Scroll down to t's\n# snip ...\ntermusic = callPackage ../applications/autio/termusic { };\n\n# we add our package here\ntestPackage = callPackage ../misc/testPackage { };\n\ntfk8s = callPackage ../applications/misc/tfk8s { };\n# snip ...\n</code></pre>\n<blockquote>\n<p><code>callPackage</code> is a core utility in Nixpkgs. It takes a Nix expression (like\nour <code>default.nix</code> file, which defines a function) and automatically provides\nthe function with any arguments it declares, by looking them up within the\n<code>pkgs</code> set (or the scope where <code>callPackage</code> is invoked). This means you only\nneed to list the dependencies your package needs in its <code>default.nix</code> function\nsignature, and <code>callPackage</code> will “inject” the correct versions of those\npackages. This is what the <code>callPackage</code> Nix Pill demonstrates at a lower\nlevel.</p>\n</blockquote>\n<h2>Understanding <code>pkgs/by-name/</code> and other locations</h2>\n<p>Nixpkgs uses different conventions for package placement:</p>\n<ul>\n<li>\n<p><strong>Older categories (e.g., <code>pkgs/misc/</code>, <code>pkgs/applications/</code>):</strong> Packages\nwithin these directories typically use <code>default.nix</code> as their definition file\n(e.g., <code>pkgs/misc/testPackage/default.nix</code>). <strong>These packages are NOT\nautomatically included</strong> in the top-level <code>pkgs</code> set; they <em>must</em> be This\nchapter will assume you have already have a cloned fork of Nixpkgs. explicitly\nadded via a <code>callPackage</code> entry in <code>pkgs/top-level/all-packages.nix</code>. This is\nthe method demonstrated in this chapter for our <code>testPackage</code>.</p>\n</li>\n<li>\n<p><strong>The new <code>pkgs/by-name/</code> convention:</strong> This is the <em>preferred location for\nnew packages</em>.</p>\n<ul>\n<li>\n<p>Packages here are placed in a directory structure like\n<code>pkgs/by-name/&lt;first-two-letters&gt;/&lt;package-name&gt;/</code>.</p>\n</li>\n<li>\n<p>Crucially, their main definition file is named <code>package.nix</code> (e.g.,\n<code>pkgs/by-name/te/testPackage/package.nix</code>).</p>\n</li>\n<li>\n<p><strong>Packages placed within <code>pkgs/by-name/</code> are automatically discovered and\nexposed</strong> by Nixpkgs’ top-level <code>pkgs</code> set. They <strong>do not</strong> require a manual\n<code>callPackage</code> entry in <code>all-packages.nix</code>. This results in a more modular\nand scalable approach, reducing manual maintenance.</p>\n</li>\n</ul>\n</li>\n</ul>\n<blockquote>\n<p>❗ : While this example uses <code>pkgs/misc/</code> to demonstrate explicit\n<code>callPackage</code> usage, when contributing a <em>new</em> package to Nixpkgs, you should\nnearly always place it within <code>pkgs/by-name/</code> and name its definition file\n<code>package.nix</code>.</p>\n</blockquote>\n<ul>\n<li>\n<p><a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/README.md\">pkgs/by-name/README</a></p>\n</li>\n<li>\n<p>There are some\n<a href=\"https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/README.md#limitations\">Limitations</a>\nto this approach.</p>\n</li>\n<li>\n<p><a href=\"https://github.com/NixOS/nixpkgs-vet\">nixpkgs-vet</a></p>\n</li>\n</ul>\n<p>Previously, packages were manually added to <code>all-packages.nix</code>. While this is no\nlonger needed in most cases, understanding the old method provides useful\ncontext for troubleshooting legacy configurations or custom integrations.</p>\n<h2>Try Building the Package</h2>\n<p>Move to the root directory of Nixpkgs:</p>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs\n</code></pre>\n<p>Try building it:</p>\n<pre><code class=\"language-bash\">nix-build -A testPackage\nthis derivation will be built:\nthis derivation will be built:\n  /nix/store/yrbjsxmgzkl24n75sqjfxbpv5cs3b9hc-testPackage.drv\nbuilding '/nix/store/yrbjsxmgzkl24n75sqjfxbpv5cs3b9hc-testPackage.drv'...\n/nix/store/3012zlv30vn6ifihr1jxbg5z3ysw0hl3-testPackage\n</code></pre>\n<p><code>runCommand</code> is a simple builder, it takes 3 arguments. The first is the package\nname the second is the derivation attributes, and the third is the script to\nrun.</p>\n<pre><code class=\"language-bash\">cat ~/src/nixpkgs/result\n───────┬──────────────────────────────\n       │ File: result\n───────┼──────────────────────────────\n   1   │ This is a Test\n───────┴──────────────────────────────\n</code></pre>\n<pre><code class=\"language-bash\">nix-instantiate --eval -A testPackage.meta.position\n\"/home/jr/src/nixpkgs/pkgs/misc/testPackage/default.nix:6\"\n</code></pre>\n<p>Tools like <code>nix search</code> and the Nixpkgs website use the <code>meta</code> information for\ndocumentation and discoverability. It can also be useful for debugging and helps\nto provide better error messages. The above command shows that the\n<code>meta.position</code> attribute points to the file and line where the package\ndefinition begins, which is very useful for debugging.</p>\n<p>Typically a file will have a <code>meta</code> attribute that looks similar to the\nfollowing:</p>\n<pre><code class=\"language-nix\">meta = with lib; {\n    homepage = \"https://www.openssl.org/\";\n    description = \"A cryptographic library that implements the SSL and TLS protocols\";\n    license = licenses.openssl;\n    platforms = platforms.all;\n} // extraMeta;\n</code></pre>\n<p>For example, the following shows how Nix is able to discover different parts of\nyour configuration:</p>\n<p>Launch the <code>nix repl</code> and load your local flake:</p>\n<pre><code class=\"language-bash\">cd /src\nnix repl\nnix-repl&gt; :lf nixpkgs\nnix-repl&gt; outputs.legacyPackages.x86_64-linux.openssl.meta.position\n\"/nix/store/syvnmj3hhckkbncm94kfkbl76qsdqqj3-source/pkgs/development/libraries/openssl/default.nix:303\"\nnix-repl&gt; builtins.unsafeGetAttrPos \"description\" outputs.legacyPackages.x86_64-linux.openssl.meta\n{\n  column = 9;\n  file = \"/nix/store/syvnmj3hhckkbncm94kfkbl76qsdqqj3-source/pkgs/development/libraries/openssl/default.nix\";\n  line = 303;\n}\n</code></pre>\n<p>Lets create just the <code>meta.description</code> for demonstration purposes.</p>\n<h2>Adding the meta attribute</h2>\n<p>Since we don’t have a <code>meta</code> attribute this points to a default value that’s\nincorrect.</p>\n<p>Let’s add the <code>meta</code> attribute and try it again:</p>\n<pre><code class=\"language-nix\"># default.nix\n{\n  runCommand,\n  coreutils,\n}:\nrunCommand \"testPackage\" {\n  nativeBuildInputs = [\n    coreutils\n  ];\n\n  meta = {\n    description = \"test package\";\n};\n} ''\n\n  echo 'This is a Test' &gt; $out\n''\n</code></pre>\n<pre><code class=\"language-nix\">nix-instantiate --eval -A testPackage.meta.position\n\"/home/jr/src/nixpkgs/pkgs/misc/testPackage/default.nix:11\"\n</code></pre>\n<p>Now it points us to the 11’th line, right where our <code>meta.description</code> is.</p>\n<p>Let’s stage our package so nix recognises it:</p>\n<pre><code class=\"language-bash\">cd ~/nixpkgs\ngit add pkgs/misc/testPackage/\nnix edit .#testPackage\n</code></pre>\n<p>I used <code>nix edit</code> here to ensure it was picked up properly.</p>\n<p>The <code>default.nix</code> that we’ve been working on should open in your <code>$EDITOR</code></p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/nixpkgs/overlay.html",
      "url": "https://saylesss88.github.io/nixpkgs/overlay.html",
      "title": "Nixpkgs Overlays",
      "content_html": "<h1>Nixpkgs Overlays</h1>\n<p>The following is done with a local clone of Nixpkgs located at <code>~/src/nixpkgs</code>.</p>\n<p>In this example, we will create an overlay to override the version of\n<code>btrfs-progs</code>. In the root directory of our local clone of Nixpkgs\n(i.e.<code>~/src/nixpkgs</code>) we can run the following command to locate <code>btrfs-progs</code>\nwithin Nixpkgs:</p>\n<pre><code class=\"language-bash\">fd 'btrfs-progs' .\n./pkgs/by-name/bt/btrfs-progs/\n</code></pre>\n<p>Open the <code>package.nix</code> in the above directory and copy the <code>src</code> block within\nthe <code>stdenv.mkDerivation</code> block like so:</p>\n<pre><code class=\"language-nix\"># package.nix\n  version = \"6.14\";\n\n  src = fetchurl {\n    url = \"mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz\";\n    hash = \"sha256-31q4BPyzbikcQq2DYfgBrR4QJBtDvTBP5Qzj355+PaE=\";\n  };\n</code></pre>\n<p>When we use the above <code>src</code> block in our overlay we’ll need to add\n<code>src = self.fetchurl</code> for our overlay to have access to <code>fetchurl</code>.</p>\n<p>We will replace the version with our desired version number. To find another\nversion that actually exists we need to check their github repos\n<a href=\"https://github.com/kdave/btrfs-progs/releases\">btrfs-progs Releases</a>. I can see\nthat the previous version was <code>v6.13</code>, lets try that.</p>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs\nhx overlay.nix\n</code></pre>\n<p>We will change the version to <code>6.13</code> for demonstration purposes. All that is\nreally required is changing the version and 1 character in the <code>hash</code> which\nwould cause a refetch and recalculation of the hash. We will use an empty string\nto follow convention:</p>\n<pre><code class=\"language-nix\"># overlay.nix\nself: super: {\n  btrfs-progs = super.btrfs-progs.overrideAttrs (old: rec {\n      version = \"6.13\";\n\n      # Notice the `self` added here\n      src = self.fetchurl {\n        url = \"mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz\";\n        hash = \"\";\n      };\n    };\n  });\n}\n</code></pre>\n<p>To build this with the file right from the root of the local Nixpkgs (i.e.\n<code>~/src/nixpkgs</code>) you could run the following. Running the command this way\navoids the impurity of looking it up in the <code>~/.config</code> directory:</p>\n<pre><code class=\"language-bash\">nix-build -A btrfs-progs --arg overlays '[ (import ./overlay.nix) ]'\n</code></pre>\n<p>The compiler will give you back the correct <code>hash</code>:</p>\n<pre><code class=\"language-bash\">specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\ngot:    sha256-ZbPyERellPgAE7QyYg7sxqfisMBeq5cTb/UGx01z7po=\n</code></pre>\n<p>Replace the empty <code>hash</code> with the new hash value we just got from the compiler\nso the <code>overlay.nix</code> would look like this:</p>\n<pre><code class=\"language-nix\">self: super: {\n  btrfs-progs = super.btrfs-progs.overrideAttrs (old: rec {\n    version = \"6.13\";\n\n    src = self.fetchurl {\n      url = \"mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz\";\n      hash = \"sha256-ZbPyERellPgAE7QyYg7sxqfisMBeq5cTb/UGx01z7po=\";\n    };\n  });\n}\n</code></pre>\n<p>Try building it again:</p>\n<pre><code class=\"language-bash\">nix-build -A btrfs-progs --arg overlays '[ (import ./overlay.nix) ]'\nchecking for references to /build/ in /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13...\ngzipping man pages under /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/share/man/\npatching script interpreter paths in /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13\n/nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/bin/fsck.btrfs: interpreter directive changed from \"#!/bin/sh -f\" to \"/nix/store/xy4jjgw87sbgwylm5kn047d9gkbhsr9x-bash-5.2p37/bin/sh -f\"\nstripping (with command strip and flags -S -p) in  /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/lib /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/bin\n/nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13\n</code></pre>\n<p>We can inspect it with the repl:</p>\n<pre><code class=\"language-bash\">cd ~/src/nixpkgs\nnix repl\nnix-repl&gt; :a import ./. { overlays = [ (import ./overlay.nix) ]; }\nnix-repl&gt; btrfs-progs\n«derivation /nix/store/6yxhj84cwcsnrd87rcxbd6w08l9ikc6p-btrfs-progs-6.13.drv»\nnix-repl&gt; btrfs-progs.drvAttrs.buildInputs\n[\n  «derivation /nix/store/yg4llzkcla5rppv8r1iikyamfxg3g4sg-acl-2.3.2.drv»\n  «derivation /nix/store/vqczbcwjnid6bs4cv3skl7kyd6kkzcfx-attr-2.5.2.drv»\n  «derivation /nix/store/xrvx0azszpdh2x0lnldakqx25vfxab19-e2fsprogs-1.47.2.drv»\n  «derivation /nix/store/iil4b8adk615zhp6wmzjx16z1v2f8f4j-util-linux-minimal-2.41.drv»\n  «derivation /nix/store/wwld8wp91m26wz69gp8vzh090sh5ygxd-lzo-2.10.drv»\n  «derivation /nix/store/w4ncw24gdfkbx9779xpgjli5sagi506m-systemd-minimal-libs-257.5.drv»\n  «derivation /nix/store/dmh4lvmq6n8hy56q93kplvnfnlwqzzv5-zlib-1.3.1.drv»\n  «derivation /nix/store/h8iwhnr636dwb72qqcyzp111ajjxgzr2-zstd-1.5.7.drv»\n]\nnix-repl&gt; btrfs-progs.drvAttrs.version\n\"6.13\"\nnix-repl&gt; btrfs-progs.drvAttrs.src\n«derivation /nix/store/y5nkz1xczxha4xl93qq3adndyc46dcvf-btrfs-progs-v6.13.tar.xz.drv»\n</code></pre>\n<p>Using <code>:a</code> adds the attributes from the resulting set into scope and avoids\nbringing the entire <code>nixpkgs</code> set into scope.</p>\n<p>To see whats available, you can for example type <code>btrfs-progs.drvAttrs.</code> then\nhit <code>TAB</code>.</p>\n<p>Another way to do this is to move our overlay to the\n<code>~/.config/nixpkgs/overlays</code> directory and rename the file like the following,\nagian this adds an impurity because it relies on your <code>~/.config</code> directory\nwhich is different from user to user:</p>\n<pre><code class=\"language-bash\">mv overlay.nix ~/.config/nixpkgs/overlays/btrfs-progs.nix\ncd ~/src/nixpkgs\nnix-build -A btrfs-progs\nchecking for references to /build/ in /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13...\ngzipping man pages under /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/share/man/\npatching script interpreter paths in /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13\n/nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/bin/fsck.btrfs: interpreter directive changed from \"#!/bin/sh -f\" to \"/nix/store/xy4jjgw87sbgwylm5kn047d9gkbhsr9x-bash-5.2p37/bin/sh -f\"\nstripping (with command strip and flags -S -p) in  /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/lib /nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13/bin\n/nix/store/szd6lizahidjniz85a0g1wsrfknirhwb-btrfs-progs-6.13\n</code></pre>\n<h2>Overlays with Flakes</h2>\n<p>In a flake, overlays are defined in the <code>outputs.overlays</code> attribute set of the\n<code>flake.nix</code>.</p>\n<p>They are then applied to <code>nixpkgs</code> inputs using\n<code>inputs.nixpkgs.follows = \"nixpkgs\";</code> (or similar) and the overlays attribute on\nthe input.</p>\n<p>Example of flake usage:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"My NixOS flake with custom overlays\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n  };\n\n  outputs = { self, nixpkgs, ... }: {\n\n    overlays.myCustomOverlay = final: prev: {\n      btrfs-progs = prev.btrfs-progs.overrideAttrs (old: rec {\n        version = \"6.13\";\n        src = self.fetchurl {\n          url = \"mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz\";\n          hash = \"sha256-ZbPyERellPgAE7QyYg7sxqfisMBeq5cTb/UGx01z7po=\";\n        };\n      });\n    };\n\n    nixosConfigurations.my-system = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      modules = [\n        # Apply the overlay\n        { nixpkgs.overlays = [ self.overlays.myCustomOverlay ]; }\n        ./configuration.nix\n      ];\n    };\n  };\n}\n</code></pre>\n<pre><code class=\"language-bash\">nix flake show\npath:/home/jr/btrfs-progs?lastModified=1749655369&amp;narHash=sha256-ln6dLiqo7TxStQSXgcIwfbdt7STGw4ZHftZRfWpY/JQ%3D\n├───nixosConfigurations\n│   └───my-system: NixOS configuration\n└───overlays\n    └───myCustomOverlay: Nixpkgs overlay\n</code></pre>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Debugging_and_Tracing_NixOS_Modules_9.html",
      "url": "https://saylesss88.github.io/Debugging_and_Tracing_NixOS_Modules_9.html",
      "title": "Debugging NixOS modules",
      "content_html": "<h1>Chapter 11</h1>\n<p>This chapter covers debugging NixOS modules, focusing on tracing module options\nand evaluating merges.</p>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<h2>Debugging and Tracing NixOS Modules</h2>\n<p><img src=\"https://saylesss88.github.io/images/coding4.png\" alt=\"404\" /></p>\n<!-- ![gruv17](images/gruv17.png) -->\n<ul>\n<li>Other related post if you haven’t read my previous post on modules, that may\nbe helpful before reading this one:\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/posts/nix_modules_explained/\">nix-modules-explained</a></p>\n</li>\n<li>\n<p>This post is my notes following Nix Hour 40. If it seems a little chaotic,\ntry watching one. They are hard to follow if you’re not extremely familiar\nwith the concepts.</p>\n</li>\n<li>\n<p><a href=\"https://www.youtube.com/watch?v=aLy8id4wr-M&amp;t=2120s\">Nix Hour 40</a></p>\n</li>\n</ul>\n</li>\n</ul>\n<p>Nix Code is particularly hard to <strong>debug</strong> because of (e.g. lazy evaluation,\ndeclarative nature, layered modules)</p>\n<ul>\n<li>The following simple Nix code snippet illustrates a basic NixOS module\ndefinition and how options are declared and configured. We’ll use this example\nto demonstrate fundamental debugging techniques using <code>nix-instantiate</code>.</li>\n</ul>\n<pre><code class=\"language-nix\">let\n  lib = import &lt;nixpkgs/lib&gt;;\nin\nlib.evalModules {\n  modules = [\n    ({ lib, ... }: {\n      options.foo = lib.mkOption {\n        # type = lib.types.raw;\n        type = lib.types.anything;\n        # default = pkgs;\n      };\n      config.foo = {\n        bar = 10;\n        list = [1 2 3 ];\n        baz = lib.mkDefault \"baz\";\n      };\n    })\n    {\n      foo.baz = \"bar\";\n    }\n  ];\n}\n</code></pre>\n<ul>\n<li>\n<p>In the above code, adding <code>lib</code> to the function arguments isn’t required but\nif you were to move the module to another file it would fail without it\nbecause <code>lib</code> comes from outside of it. So it’s good practice to refer to\n<code>lib</code> in the modules themselves.</p>\n</li>\n<li>\n<p>You should <strong>always</strong> assign a type to your options, if you don’t know which\ntype to use you could use <code>raw</code>. <code>raw</code> is a type that doesn’t do any\nprocessing. So if you were to assign the entire packages set to the option\ne.g. <code>default = pkgs;</code> it wouldn’t recurseinto all the packages and try to\nevaluate them. There is also <code>anything</code>, that is useful if you do want to\nrecurse into the values.</p>\n</li>\n<li>\n<p>The following is an example of how you would run this inside vim/neovim, the\nrest of the examples will be from the command line:</p>\n</li>\n</ul>\n<pre><code class=\"language-vim\">:!nix-instantiate --eval -A config.foo --strict\n</code></pre>\n<p><strong>Output</strong>:</p>\n<details>\n<summary> Click to Expand the Output </summary>\n<pre><code class=\"language-bash\">{ bar = 10; baz = \"bar\"; list = [ 1 2 3 ]; }\n</code></pre>\n<p>To show the difference you could uncomment the <code>raw</code> type and comment the\n<code>anything</code> type and run the above command again you’ll see that you get an\nerror:</p>\n<pre><code class=\"language-bash\">error: The option 'foo' is defined multiple times while it's expected to be\nunique\n</code></pre>\n<p>To execute this command on the command line:</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval --strict -A config.foo\n</code></pre>\n<p>It will show you the start of a trace. To get the full trace add:</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval --strict -A config.foo --show-trace\n</code></pre>\n</details>\n<h2>Example 2</h2>\n<details>\n<summary> Click to Expand Example 2 </summary>\n<p>In the previous example, we looked at a simplified module. Now, let’s examine a\nmore realistic scenario involving a basic NixOS configuration file\n(<code>configuration.nix</code>).</p>\n<p>This example will demonstrate how to use <code>nix-instantiate</code> to evaluate an entire\nsystem configuration and how <code>--show-trace</code> helps in diagnosing errors within\nthis context.</p>\n<p>Consider the following <code>configuration.nix</code> file:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{ lib, ... }: {\n  boot.loader.grub.device = \"nodev\";\n  fileSystems.\"/\".device = \"/devst\";\n  system.stateVersion = \"24.11\";\n}\n</code></pre>\n<ul>\n<li>This configuration snippet sets the GRUB bootloader device, defines a root\nfilesystem, and specifies the expected NixOS state version. To evaluate this\nentire system configuration, you can use <code>nix-instantiate</code> and point it to the\n<code>&lt;nixpkgs/nixos&gt;</code> entrypoint, providing our <code>configuration.nix</code> file as an\nargument. The <code>-A system</code> flag selects the top-level <code>system</code> attribute, which\nrepresents the instantiated system configuration.</li>\n</ul>\n<p><strong>Run</strong> it in with:</p>\n<pre><code class=\"language-bash\">nix-instantiate '&lt;nixpkgs/nixos&gt;' --arg configuration ./configuration.nix -A system\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">/nix/store/kfcwvvpdbsb3xcks1s76id16i1mc3l5k-nixos-system-nixos-25.05pre-git.drv\n</code></pre>\n<p>Ok, we can see that this successfully <em>instantiates</em>. Let’s introduce an error\nto trace:</p>\n<pre><code class=\"language-nix\">{ lib, ... }: {\n  boot.loader.grub.device = \"nodev\";\n  fileSystems.\"/\".device = \"/devst\";\n  system.stateVersion = builtins.genList \"24.11\" null;\n}\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">(stack trace truncated; use '--show-trace' to show the full, detailed trace)\nerror: expected an integer but found null: null\n</code></pre>\n<p>Rerun the command with <code>--show-trace</code> appended:</p>\n<p>Or on the command line</p>\n<pre><code class=\"language-bash\">nix-instantiate '&lt;nixpkgs/nixos&gt;' --arg configuration ./configuration.nix -A system --show-trace\n</code></pre>\n<ul>\n<li>This outputs a much longer trace than the first example. It shows you the file\nthe error occured in and you can see that in this case they are a lot of\ninternal functions. (e.g.\n<code>at /nix/store/ccfwxygjrarahgfv5865x2f828sjr5h0- source/lib/attrsets.nix:1529:14:</code>)</li>\n</ul>\n<p>To show your own error message you could do something like this:</p>\n<pre><code class=\"language-nix\">{lib, ...}: {\n  boot.loader.grub.device = \"nodev\";\n  fileSystems.\"/\".device = \"/devst\";\n  system.stateVersion = builtins.addErrorContext \"AAAAAAAAAAAAAAAAA\" (builtins.genList \"24.11\" null);\n}\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">nix-instantiate '&lt;nixpkgs/nixos&gt;' --arg configuration ./configuration.nix -A system --show-trace`\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\"> … while evaluating the attribute 'value'\n     at /nix/store/ccfwxygjrarahgfv5865x2f828sjr5h0-source/lib/modules.nix:770:21:\n      769|             inherit (module) file;\n      770|             inherit value;\n         |                     ^\n      771|           }) module.config\n\n   … AAAAAAAAAAAAAAAAA\n\n   … while calling the 'genList' builtin\n     at /home/jr/tests/configuration.nix:4:71:\n        3|   fileSystems.\"/\".device = \"/devst\";\n        4|   system.stateVersion = builtins.addErrorContext \"AAAAAAAAAAAAAAAAA\"\n         (builtins.genList \"24.11\" null);\n         |                                                                       ^\n        5| }\n\n   … while evaluating the second argument passed to builtins.genList\n\n   error: expected an integer but found null: null\n</code></pre>\n<ul>\n<li>In the latest nix they actually inverted the error messages so the most\nrelevant parts will be at the bottom.</li>\n</ul>\n</details>\n<h2>Example 3</h2>\n<details>\n<summary> Click to Expand Example 3 </summary>\n<p>Let’s consider another example, this time demonstrating the definition of\nconfiguration options using <code>lib.mkOption</code> within a module structure.</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  lib = import &lt;nixpkgs/lib&gt;;\nin\nlib.evalModules {\n  modules = [\n    ({ lib, ... }: {\n      options.ints = lib.mkOption {\n        type = lib.types.attrsOf lib.types.int;\n      };\n      options.strings = lib.mkOption {\n        type = lib.types.string;\n        # type = lib.types.attrsOf lib.types.string;\n        default = \"foo\";\n      };\n    })\n  ];\n}\n</code></pre>\n<p><strong>Instantiate</strong> this with:</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval --strict -A config.strings\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">evaluation warning: The type `types.string` is deprecated.\nSee https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types.\n\"foo\"\n</code></pre>\n<ul>\n<li>Unfortunately you won’t get the same depreciation warning from <code>lib.attrsOf</code></li>\n</ul>\n<p>Below is an interesting way to provide nixpkgs run it on the command line:</p>\n<pre><code class=\"language-bash\">export NIX_PATH=nixpkgs=channel:nixpkgs-unstable\necho $NIX_PATH\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">nixpkgs=channel:nixpkgs-unstable\n</code></pre>\n<p>The next two commands are to check that after using the above way to provide\n<code>nixpkgs-unstable</code> that they both point to the same store path, the following\ncommand will fetch nixpkgs from the channel above:</p>\n<pre><code class=\"language-bash\">nix-instantiate --find-file nixpkgs\n</code></pre>\n<p><strong>Output</strong> 1️⃣</p>\n<pre><code class=\"language-bash\">/nix/store/ydrgwsibghsyx884qz97zbs1xs93yk11-source\n</code></pre>\n<pre><code class=\"language-bash\">nix-instantiate --eval channel:nixpkgs-unstable -A path\n</code></pre>\n<p><strong>Output</strong>: 2️⃣</p>\n<pre><code class=\"language-bash\">/nix/store/ydrgwsibghsyx884qz97zbs1xs93yk11-source\n</code></pre>\n<ul>\n<li>As you can see both commands produce the same store path</li>\n</ul>\n<h2>Example 4</h2>\n<p>In our previous example, we encountered a deprecation warning for\n<code>lib.types.string</code>. This next example delves deeper into why that type was\ndeprecated and demonstrates the consequences of its behavior, along with the\nrecommended fix.</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ({lib, ...}: {\n        options.ints = lib.mkOption {\n          type = lib.types.attrsOf lib.types.int;\n        };\n        options.strings = lib.mkOption {\n          # type = lib.types.string;\n          type = lib.types.attrsOf lib.types.string;\n          default = {\n            x = \"foo\";\n          };\n        };\n        config = {\n          strings = lib.mkOptionDefault {\n            x = \"bar\";\n          };\n        };\n      })\n    ];\n  }\n</code></pre>\n<p>Evaluate it with:</p>\n<pre><code class=\"language-bash\">nix-instantiate --eval --strict -A config.strings\n</code></pre>\n<ul>\n<li>\n<p><code>types.string</code> depricated because it silently concatenates strings</p>\n</li>\n<li>\n<p>The above command has two options with the same priority level and evaluates\nto <code>{ x = \"foobar\"; }</code></p>\n</li>\n</ul>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">evaluation warning: The type `types.string` is deprecated. See https://github.\ncom/NixOS/nixpkgs/pull/66346 for better alternative types.\n{ x = \"foobar\"; }\n</code></pre>\n<ul>\n<li><code>types.str</code> was the replacement for the depricated <code>types.string</code>:</li>\n</ul>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ({lib, ...}: {\n        options.ints = lib.mkOption {\n          type = lib.types.attrsOf lib.types.int;\n        };\n        options.strings = lib.mkOption {\n          # type = lib.types.string;\n          type = lib.types.attrsOf lib.types.str;\n          # Sets the value with a lower priority: lib.mkOptionDefault\n          default = {\n            x = \"foo\";\n          };\n        };\n        config = {\n          strings = lib.mkOptionDefault {\n            x = \"bar\";\n          };\n        };\n      })\n    ];\n  }\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">error:\n… while evaluating the attribute 'x'\n\n… while evaluating the attribute 'value'\n at /nix/store/ydrgwsibghsyx884qz97zbs1xs93yk11-source/lib/modules.nix:1148:41:\n 1147|\n 1148|     optionalValue = if isDefined then { value = mergedValue; } else { };\n     |                                         ^\n 1149|   };\n\n… while calling the 'foldl'' builtin\n at /nix/store/ydrgwsibghsyx884qz97zbs1xs93yk11-source/lib/options.nix:508:8:\n  507|     else\n  508|       (foldl' (\n     |        ^\n  509|         first: def:\n\n(stack trace truncated; use '--show-trace' to show the full, detailed trace)\n\nerror: The option `strings.x' has conflicting definition values:\n- In `&lt;unknown-file&gt;': \"foo\"\n- In `&lt;unknown-file&gt;': \"bar\"\nUse `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.\n\nshell returned 1\n</code></pre>\n</details>\n<h2>Summary</h2>\n<ul>\n<li>\n<p>So types in the module system aren’t just types in the conventional sense but\nthey also specify the emerging behavior of these values.</p>\n</li>\n<li>\n<p>If we switch the type in the above example to <code>types.lines</code> you get this\nreturned, <code>{ x = \"foo\\nbar\"; }</code></p>\n</li>\n<li>\n<p><code>mkOptionDefault</code> isn’t typically something you should generally use, instead\noptions have a <code>default</code> setting</p>\n</li>\n<li>\n<p>If you want to make sure that you set a default but if the user specifies it,\nit shouldn’t get overridden. You should not set it in the following:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\">options.strings = lib.mkOption {\n  type = lib.types.attrsOf lib.types.lines;\n  default = {\n    x = \"foo\";\n  };\n}\n</code></pre>\n<p>Because the above uses <code>mkOptionDefault</code> but instead in under the <code>config</code>\nattribute like the following:</p>\n<pre><code class=\"language-nix\"># ...snip...\noptions.strings = lib.mkOption {\n  type = lib.types.attrsOf lib.types.lines;\n  # default = {\n    # x = \"foo\";\n  # };\n};\nconfig = {\n  strings = {\n    x = lib.mkDefault \"foo\";\n  };\n};\n# ...snip...\n</code></pre>\n<pre><code class=\"language-nix\">let\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ({lib, ...}: {\n        options.ints = lib.mkOption {\n          type = lib.types.attrsOf lib.types.int;\n        };\n        options.strings = lib.mkOption {\n          # type = lib.types.string;\n          type = lib.types.attrsOf lib.types.str;\n          # Sets the value with a lower priority: lib.mkOptionDefault\n          #default = {\n          #  x = \"foo\";\n          #};\n        };\n        config.strings = {\n          x = \"foo\";\n        };\n      })\n      {\n        config.strings = {\n          y = \"bar\";\n        };\n      }\n    ];\n  }\n</code></pre>\n<p><strong>Output</strong>:</p>\n<ul>\n<li>This works now because there’s no difference between <code>x</code> and <code>y</code></li>\n</ul>\n<pre><code class=\"language-bash\">{ x = \"foo\"; y = \"bar\"; }\n</code></pre>\n<h2>More Functionality between modules</h2>\n<pre><code class=\"language-nix\">let\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ({lib, ...}: {\n        options.ints = lib.mkOption {\n          type = lib.types.attrsOf lib.types.int;\n        };\n        options.strings = lib.mkOption {\n          # type = lib.types.string;\n          type = lib.types.attrsOf lib.types.str;\n          # Sets the value with a lower priority: lib.mkOptionDefault\n          #default = {\n          #  x = \"foo\";\n          #};\n        };\n        config.strings = {\n          x = lib.mkDefault \"foo\";\n        };\n      })\n      {\n        config.strings = {\n          x = \"x\";\n          y = \"bar\";\n        };\n      }\n    ];\n  }\n</code></pre>\n<ul>\n<li>The above command would cause a conflict without the <code>x = lib.mkDefault foo</code>\nAnd this is typically what you want to do for defaults and modules in things\nlike nested configuration.</li>\n</ul>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">{ x = \"x\"; y = \"bar\"; }\n</code></pre>\n<h3>Infinite recursion error</h3>\n<ol>\n<li>A common pitfall is to introduce a hard to debug error <code>infinite recursion</code>\nwhen shadowing a name. The simplest example for this is:</li>\n</ol>\n<blockquote>\n<pre><code class=\"language-nix\">let a = 1; in rec { a = a; }\n</code></pre>\n</blockquote>\n<blockquote>\n<p>💡<strong>TIP</strong>: Avoid <code>rec</code>. Use <code>let ... in</code> Example:</p>\n<pre><code class=\"language-nix\">let\n a = 1;\nin {\n a = a;\n b = a + 2;\n}\n</code></pre>\n</blockquote>\n<details>\n<summary> Click to Expand a more involved infinite recursion error </summary>\n<p>We’ll separate the logic for this example, this will be the <code>default.nix</code> this\nis where having <code>lib</code> defined in your inline modules is helpful because you can\njust delete the section and paste it into your <code>modules.nix</code>:</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ./module.nix\n    ];\n  }\n</code></pre>\n<p>And in the <code>module.nix</code>:</p>\n<pre><code class=\"language-nix\"># module.nix\n{ lib, pkgs, ...}: {\n  options.etc = lib.mkOption {\n    type = lib.types.attrsOf lib.types.path;\n    default = { };\n    description = ''\n      Specifies which paths are is /etc/\n    '';\n  };\n\n  config._module.args.pkgs = import &lt;nixpkgs&gt; {\n    config = {};\n    overlays = [];\n  };\n  config.etc.foo = pkgs.writeText \"foo\" ''\n    foo configuration\n  '';\n}\n</code></pre>\n<ul>\n<li>If you evaluate this with the following you will get an infinite recursion\nerror.</li>\n</ul>\n<pre><code class=\"language-bash\">nix-instantiate --eval --strict -A config.etc\n</code></pre>\n<ul>\n<li>This happens because <code>--strict</code> evaluates the <code>etc</code>, then it goes into the\n<code>attrsOf</code>, and the <code>path</code></li>\n</ul>\n<pre><code class=\"language-bash\">nix repl\nnix-repl&gt; :l &lt;nixpkgs&gt;\nnix-repl&gt; hello.out.out.out\n</code></pre>\n<p>In this example:</p>\n<ul>\n<li>\n<p><code>:l &lt;nixpkgs&gt;</code> loads the Nixpkgs library into the repl environment, making its\ndefinitions available.</p>\n</li>\n<li>\n<p><code>hello</code> refers to the <code>hello</code> package definition within Nixpkgs. Packages in\nNixpkgs are defined as <em>derivations</em>.</p>\n</li>\n<li>\n<p><code>.out</code> is a common attribute name for the <em>main output</em> of a derivation (e.g.,\nthe installed package). Some packages, especially those with complex build\nprocesses or multiple outputs, might have nested output attributes. In the\ncase of <code>hello</code>, accessing <code>.out.out.out</code> ultimately leads us to the\n<em>derivation</em> itself.</p>\n</li>\n</ul>\n<p>The key takeaway here is that when you evaluate a package in the <code>nix repl</code>,\nyou’re often interacting with its derivation or one of its output paths in the\nNix store. The <code>«derivation ...»</code> indicates that <code>hello.out.out.out</code> evaluates\nto a derivation – the blueprint for building the <code>hello</code> package. This is in\ncontrast to <code>--eval --strict</code>, which tries to fully evaluate values, potentially\nleading to infinite recursion if it encounters a derivation that refers back to\nitself indirectly during attribute evaluation.</p>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">«derivation /nix/store/b1vcpm321dwbwx6wj4n13l35f4y2wrfv-hello-2.12.1.drv»\n</code></pre>\n<ul>\n<li>So it recurses through the entire thing and tries to evaluate its string.</li>\n</ul>\n<p>So we want to change the command from <code>--eval --strict</code> which is only based on\nevaluation to at least <code>nix-instantiate</code> which is based on derivations:</p>\n<pre><code class=\"language-bash\">nix-instantiate -A config.etc\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">warning: you did not specify '--add-root'; the result might be removed by the garbage collector\n/nix/store/abyfp1rxk73p0n5kfilv7pawxwvc7hsg-foo.drv\n</code></pre>\n<ul>\n<li>We don’t really have a derivation yet for example:</li>\n</ul>\n<pre><code class=\"language-nix\"># module.nix\n{\n  lib,\n  pkgs,\n  ...\n}: {\n  options.etc = lib.mkOption {\n    type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);\n    default = {};\n    description = ''\n      Specifies which paths are in /etc/\n    '';\n  };\n\n  config._module.args.pkgs = import &lt;nixpkgs&gt; {\n    config = {};\n    overlays = [];\n  };\n  config.etc.foo.bar = pkgs.writeText \"foo\" ''\n    foo configuration\n  '';\n}\n</code></pre>\n<p>Try to evaluate the above command with <code>nix-instantiate -A config.etc</code> and Nix\ndoesn’t even try to build it. With nested <code>attrsOf</code></p>\n<pre><code class=\"language-bash\">nix repl -f default.nix\nnix-repl&gt; config.etc\n{\n  foo = { ... };\n}\nnix-repl&gt; config.etc.foo\n{\n  bar = «derivation /nix/store/abyfp1rxk73p0n5kfilv7pawxwvc7hsg-foo.drv»;\n}\n</code></pre>\n<ul>\n<li>So <code>config.foo</code> is an attribute set and <code>config.etc.foo</code> is also an attribute\nset but it’s not a derivation by itself. So <code>nix-instantiate</code> does this one\nlevel of recursion here and it would have built <code>foo</code> value if it were a\nderivation.</li>\n</ul>\n</details>\n<h3>Example 5</h3>\n<details>\n<summary> Click to Expand Example 5 </summary>\n<p>We’ll use the same <code>module.nix</code> and <code>default.nix</code> from the previous example.</p>\n<p>Building More Complex Configurations with Modules In this next example, we’ll\nfocus on a common task in system configuration: managing files within the\n<code>/etc/</code> directory. We’ll define a module that allows us to specify the content\nof arbitrary files in <code>/etc/</code> and then use a special Nix function to combine\nthese individual file definitions into a single, manageable entity.</p>\n<p>We’ll introduce a new option, <code>options.etc</code>, which will allow us to define the\ncontent of files within <code>/etc/</code>. Then, we’ll use <code>pkgs.linkFarm</code> to create a\nderivation that represents the entire <code>/etc/</code> directory as a collection of\nsymbolic links pointing to the individual file contents we’ve defined. This\ndemonstrates how modules can abstract away the details of creating complex\nsystem configurations, providing a declarative and reproducible way to manage\neven fundamental aspects of the operating system.</p>\n<p>Let’s show how we can use Nix modules to declaratively manage the <code>/etc/</code>\ndirectory</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  lib = import &lt;nixpkgs/lib&gt;;\nin\n  lib.evalModules {\n    modules = [\n      ./module.nix\n    ];\n  }\n\n</code></pre>\n<pre><code class=\"language-nix\"># module.nix\n{\n  lib,\n  pkgs,\n  config,\n  ...\n}: {\n  options.etc = lib.mkOption {\n    type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);\n    default = {};\n    description = ''\n      Specifies which paths are in /etc/\n    '';\n  };\n  options.etcCombined = lib.mkOption {\n    type = lib.types.package;\n    default =\n      pkgs.linkFarm \"etc\"\n      (lib.mapAttrsToList (name: value: {\n        name = name;\n        path = value;\n      }) config.etc);\n  };\n\n  config._module.args.pkgs = import &lt;nixpkgs&gt; {\n    config = {};\n    overlays = [];\n  };\n  config.etc.foo = pkgs.writeText \"foo\" ''\n    foo configuration\n  '';\n  config.etc.bar = pkgs.writeText \"bar\" ''\n    bar configuration\n  '';\n}\n\n</code></pre>\n<p>Run it with:</p>\n<pre><code class=\"language-bash\">nix-instantiate -A config.etcCombined\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">/nix/store/3da61nmfk546qn2zpxsm57mq6vz6fjx8-etc.drv\n</code></pre>\n<ul>\n<li>So we can see that it will instantiate, lets see if it will build:</li>\n</ul>\n<pre><code class=\"language-bash\">nix-build -A config.etcCombined\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">these 3 derivations will be built:\n/nix/store/41yfxq4af1vrs0rrgfk5gc36kmjc7270-bar.drv\n/nix/store/abyfp1rxk73p0n5kfilv7pawxwvc7hsg-foo.drv\n/nix/store/3da61nmfk546qn2zpxsm57mq6vz6fjx8-etc.drv\nbuilding '/nix/store/41yfxq4af1vrs0rrgfk5gc36kmjc7270-bar.drv'...\nbuilding '/nix/store/abyfp1rxk73p0n5kfilv7pawxwvc7hsg-foo.drv'...\nbuilding '/nix/store/3da61nmfk546qn2zpxsm57mq6vz6fjx8-etc.drv'...\n/nix/store/ca3wyk5m3qhy8n1nbn0181m29qvp1klp-etc\n</code></pre>\n<pre><code class=\"language-bash\">nix-build -A config.etcCombined &amp;&amp; ls result/ -laa\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">/nix/store/ca3wyk5m3qhy8n1nbn0181m29qvp1klp-etc\ndr-xr-xr-x - root 31 Dec  1969  .\ndrwxrwxr-t - root 16 May 15:13  ..\nlrwxrwxrwx - root 31 Dec  1969  bar -&gt; /nix/store/1fsjyc2hmilab1qw6jfkf6cb767kz858-bar\nlrwxrwxrwx - root 31 Dec  1969  foo -&gt; /nix/store/wai5dycp0zx1lxg0rhpdxnydhiadpk05-foo\n</code></pre>\n<ul>\n<li>\n<p>We can see that <code>foo</code> and <code>bar</code> link to different derivations</p>\n</li>\n<li>\n<p>When trying to figure out which <code>default</code> to use for <code>etcCombined</code> infinisil\nwent to the Nixpkgs Reference Manual. Make sure to go to the correct version.</p>\n<ul>\n<li>\n<p><a href=\"https://nixos.org/manual/nixpkgs/stable/\">24.11pre-git</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/manual/nixpkgs/unstable/\">25.05pre-git</a> (i.e. unstable)</p>\n</li>\n<li>\n<p>Once at the website press <code>Ctrl+f</code> and type <code>symlinkjoin</code> and hit enter.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>Or in your local copy of Nixpkgs you could go to\n<code>nixpkgs/pkgs/build-support/ trivial-builders/default.nix</code>. Then use your\neditors search feature, with nvim and helix you press <code>/symlinkjoin</code> or\n<code>/linkFarm</code> hit enter then press <code>n</code> to cycle to the next match. It will bring\nyou to comments and up to date information.</p>\n<pre><code class=\"language-bash\"># linkFarm \"myexample\" [ { name = \"hello-test\"; path = pkgs.hello; }\n# { name = \"foobar\"; path = pkgs.stack; } ]\n</code></pre>\n</details>\n<h3>Tests</h3>\n<details>\n<summary> Click to Expand Test Example </summary>\n<ul>\n<li>How to create a Derivation with <code>passthru.tests</code> outside of Nixpkgs and then\nrun tests available to your package set?</li>\n</ul>\n<pre><code class=\"language-bash\">mkdir passthru-tests &amp;&amp; cd passthru-tests\n</code></pre>\n<p>Create a <code>default.nix</code> with the following:</p>\n<pre><code class=\"language-nix\"># default.nix\nlet\n  pkgs = import &lt;nixpkgs&gt; {};\n\n  package = pkgs.runCommand \"foo\" {\n    passthru.tests.simple = pkgs.runCommand \"foo-test\" {} ''\n      if [[ \"$(cat ${package})\" != \"foo\" ]]; then\n        echo \"Result is not foo\"\n        exit 1\n      fi\n      touch $out\n  '';\n  } ''\n    echo foo &gt; $out\n  '';\nin\npackage\n</code></pre>\n<p>See if it will build:</p>\n<pre><code class=\"language-bash\">nix-build\n</code></pre>\n<p>Try running the test:</p>\n<pre><code class=\"language-bash\">nix-build -A passthru.tests\n</code></pre>\n<pre><code class=\"language-bash\">this derivation will be built:\n/nix/store/pqpqq9x1wnsabzbsb52z4g4y4zy6p7yx-foo-test.drv\nbuilding '/nix/store/pqpqq9x1wnsabzbsb52z4g4y4zy6p7yx-foo-test.drv'...\n/nix/store/7bbw2ban0mgkh4d59yz3cnai4aavwvb6-foo-test\n</code></pre>\n<h3>Test 2</h3>\n<ul>\n<li><code>passthru.tests</code> is the convention for defining tests associated with a\nderivation. The attributes in <code>passthru</code> are preserved and accessible after\nthe derivation is built.</li>\n</ul>\n<pre><code class=\"language-nix\">let\n  pkgs = import &lt;nixpkgs&gt; {};\n\n  package =\n    pkgs.runCommand \"foo\" {\n      passthru.tests.simple = pkgs.runCommand \"foo-test\" {} ''\n        if [[ \"$(cat ${package})\" != \"foo\" ]]; then\n          echo \"Result is not foo\"\n          exit 1\n        fi\n        touch $out\n      '';\n\n      passthru.tests.version = pkgs.testers.testVersion {\n         package = package;\n         version = \"1.2\";\n     };\n\n      # pkgs.writeShellApplication\n      script = ''\n        #!${pkgs.runtimeShell}\n        echo \"1.2\"\n      '';\n      passAsFiles = [ \"script\" ];\n\n    } ''\n      cp \"$scriptPath\" \"$out\"\n    '';\nin\n  package\n</code></pre>\n<p>Try to build it:</p>\n<pre><code class=\"language-bash\">nix-build -A passthru.tests\n</code></pre>\n<ul>\n<li>\n<p><code>testers.testVersion</code> checks if an executable outputs a specific version\nstring.</p>\n</li>\n<li>\n<p><code>nix-build -A passthru.tests</code> specifically targets the derivations defined\nwithin the tests attribute of the main derivation.</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">these 3 derivations will be built:\n  /nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv\n  /nix/store/s4iawjy5zpv89dbkc3zz7z3ngz4jq2cv-foo-test.drv\n  /nix/store/z3gi4pb8jn2h9rvk4dhba85fiphp5g4z-foo-test-version.drv\nbuilding '/nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv'...\ncp: cannot stat '': No such file or directory\nerror: builder for '/nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv'\n failed with exit code 1;\n     last 1 log lines:\n     &gt; cp: cannot stat '': No such file or directory\n     For full logs, run:\n       nix log /nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv\nerror: 1 dependencies of derivation '/nix/store/z3gi4pb8jn2h9rvk4dhba85fiphp5g4z\n-foo-test-version.drv' failed to build\nerror: build of '/nix/store/s4iawjy5zpv89dbkc3zz7z3ngz4jq2cv-foo-test.drv',\n '/nix/store/z3gi4pb8jn2h9rvk4dhba85fiphp5g4z-foo-test-version.drv' failed\n</code></pre>\n<p>Run <code>nix-build</code> with no arguments:</p>\n<pre><code class=\"language-bash\">nix-build\n</code></pre>\n<pre><code class=\"language-bash\">nix derivation show /nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv | jq '.[].env'\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-json\">{\n  \"__structuredAttrs\": \"\",\n  \"buildCommand\": \"cp \\\"$scriptPath\\\" \\\"$out\\\"\\n\",\n  \"buildInputs\": \"\",\n  \"builder\": \"/nix/store/xg75pc4yyfd5n2fimhb98ps910q5lm5n-bash-5.2p37/bin/bash\",\n  \"cmakeFlags\": \"\",\n  \"configureFlags\": \"\",\n  \"depsBuildBuild\": \"\",\n  \"depsBuildBuildPropagated\": \"\",\n  \"depsBuildTarget\": \"\",\n  \"depsBuildTargetPropagated\": \"\",\n  \"depsHostHost\": \"\",\n  \"depsHostHostPropagated\": \"\",\n  \"depsTargetTarget\": \"\",\n  \"depsTargetTargetPropagated\": \"\",\n  \"doCheck\": \"\",\n  \"doInstallCheck\": \"\",\n  \"enableParallelBuilding\": \"1\",\n  \"enableParallelChecking\": \"1\",\n  \"enableParallelInstalling\": \"1\",\n  \"mesonFlags\": \"\",\n  \"name\": \"foo\",\n  \"nativeBuildInputs\": \"\",\n  \"out\": \"/nix/store/9mcrnddb6lf1md14v4lj6s089i99l5k7-foo\",\n  \"outputs\": \"out\",\n  \"passAsFile\": \"buildCommand\",\n  \"passAsFiles\": \"script\",\n  \"patches\": \"\",\n  \"propagatedBuildInputs\": \"\",\n  \"propagatedNativeBuildInputs\": \"\",\n  \"script\": \"#!/nix/store/xg75pc4yyfd5n2fimhb98ps910q5lm5n-bash-5.2p37/bin/bash\\necho \\\"1.2\\\"\\n\",\n  \"stdenv\": \"/nix/store/lgydi1gl5wqcw6k4gyjbaxx7b40zxrsp-stdenv-linux\",\n  \"strictDeps\": \"\",\n  \"system\": \"x86_64-linux\"\n}\n</code></pre>\n<pre><code class=\"language-bash\">nix derivation show /nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv | jq\n '.[].env.buildCommand'\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">\"cp \\\"$scriptPath\\\" \\\"$out\\\"\\n\"\n</code></pre>\n<ul>\n<li>raw mode below</li>\n</ul>\n<pre><code class=\"language-bash\">nix derivation show /nix/store/lyz86bd78p7f3yjy1qky6annmggymcwd-foo.drv | jq\n '.[].env.buildCommand' -r\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-bash\">cp \"$scriptPath\" \"$out\"\n</code></pre>\n<ul>\n<li>It turns out the correct command was <code>passAsFile</code> not <code>passAsFiles</code> but that\nchange wasn’t enough to fix it. <code>passAsFiles</code> expects a list of files, not a\nsingle file path. Running <code>nix-build -A passthru.tests</code> failed saying\n<code>&gt; foo --version returned a non-zero exit code.</code></li>\n</ul>\n<pre><code class=\"language-nix\">let\n  pkgs = import &lt;nixpkgs&gt; {};\n\n  package =\n    pkgs.runCommand \"foo\" {\n      #passthru.tests.simple = pkgs.runCommand \"foo-test\" {} ''\n      #  if [[ \"$(cat ${package})\" != \"foo\" ]]; then\n      #    echo \"Result is not foo\"\n      #    exit 1\n      #  fi\n      #  touch $out\n      #'';\n\n      passthru.tests.version = pkgs.testers.testVersion {\n        package = package;\n        version = \"1.2\";\n      };\n\n      # pkgs.writeShellApplication\n      script = ''\n        #!${pkgs.runtimeShell}\n        echo \"1.2\"\n      '';\n      passAsFile = [\"script\"];\n    } ''\n      mkdir -p \"$out/bin\"\n      cp \"$scriptPath\" \"$out/bin/foo\"\n      chmod +x \"$out/bin/foo\"\n    '';\nin\n  package\n</code></pre>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">nix-build -A passthru.tests\n</code></pre>\n<p><strong>Output:</strong></p>\n<pre><code class=\"language-bash\">these 2 derivations will be built:\n  /nix/store/lqrlcd64dmpzkggcfzlnsnwjd339czd3-foo.drv\n  /nix/store/c3kw4xbdlrig08jrdm5wis1dmv2gnqsd-foo-test-version.drv\nbuilding '/nix/store/lqrlcd64dmpzkggcfzlnsnwjd339czd3-foo.drv'...\nbuilding '/nix/store/c3kw4xbdlrig08jrdm5wis1dmv2gnqsd-foo-test-version.drv'...\n1.2\n/nix/store/zsbk5zawak68ailvkwi2gad2bqbqmdz9-foo-test-version\n</code></pre>\n</details>\n<h3>Key Takeaways for Debugging NixOS Modules</h3>\n<ul>\n<li>\n<p><strong><code>nix-instantiate</code> is Your Friend:</strong> Use <code>nix-instantiate</code> to evaluate your\nNixOS modules and pinpoint errors.</p>\n</li>\n<li>\n<p><strong>Unlock Details with <code>--show-trace</code>:</strong> When errors occur, always append\n<code>--show-trace</code> to get a comprehensive stack trace, revealing the origin of the\nproblem. Remember that in newer Nix versions, the most relevant parts of the\ntrace are often at the bottom.</p>\n</li>\n<li>\n<p><strong>Understand Option Types:</strong> Nix option types (<code>raw</code>, <code>anything</code>,\n<code>string</code>/<code>str</code>, <code>lines</code>, <code>attrsOf</code>) are not just about data types; they also\ndictate how values are merged and processed within the module system.</p>\n</li>\n<li>\n<p><strong>Be Mindful of <code>mkOptionDefault</code>:</strong> While useful in specific scenarios,\n<code>mkOptionDefault</code> sets a lower priority default. For standard defaults that\ncan be overridden by user configuration, define them directly within the\n<code>config</code> attribute using <code>lib.mkDefault</code>.</p>\n</li>\n<li>\n<p><strong>Use <code>builtins.addErrorContext</code>:</strong> Enhance your custom error messages by\nproviding specific context relevant to your module’s logic using\n<code>builtins.addErrorContext</code>.</p>\n</li>\n<li>\n<p><strong>Derivations vs. Evaluation:</strong> Be aware of the difference between evaluating\nexpressions (<code>--eval --strict</code>) and instantiating derivations\n(<code>nix-instantiate</code>). Strict evaluation can trigger infinite recursion if it\nencounters unevaluated derivations with cyclic dependencies during attribute\naccess.</p>\n</li>\n<li>\n<p><strong>Explore with <code>nix repl</code>:</strong> The <code>nix repl</code> allows you to interactively\nexplore Nix expressions and the outputs of derivations, providing insights\ninto the structure and values within Nixpkgs.</p>\n</li>\n</ul>\n<h4>Conclusion</h4>\n<p>This chapter has equipped you with essential techniques for debugging and\ntracing NixOS modules. We’ve explored how to use <code>nix-instantiate</code> and\n<code>--show-trace</code> to pinpoint errors, how to interpret Nix’s often-verbose error\nmessages, and how to leverage the <code>nix repl</code> for interactive exploration.\nUnderstanding option types and the nuances of <code>mkOptionDefault</code> is crucial for\nwriting robust and predictable modules. We’ve also touched upon the distinction\nbetween evaluation and instantiation, and how that impacts debugging.</p>\n<p>While these tools and techniques are invaluable for understanding and\ntroubleshooting your own Nix configurations, they also become essential when you\nwant to contribute to or modify the vast collection of packages and modules\nwithin <strong>Nixpkgs</strong> itself. Nixpkgs is where the majority of Nix packages and\nNixOS modules reside, and learning how to navigate and contribute to it opens up\na whole new level of control and customization within the Nix ecosystem.</p>\n<p>In the next chapter,\n<a href=\"https://saylesss88.github.io/Working_with_Nixpkgs_Locally_10.html\">Working with Nixpkgs Locally</a>,\nwe’ll shift our focus to exploring and modifying Nixpkgs. We’ll cover how to\nclone Nixpkgs, how to make changes to package definitions, and how to test those\nchanges locally before contributing them back upstream. This chapter will\nempower you to not just use existing Nix packages, but also to customize and\nextend them to fit your specific needs.</p>\n",
      "date_published": "2025-11-22T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/NixOS_Modules_Explained_3.html",
      "url": "https://saylesss88.github.io/NixOS_Modules_Explained_3.html",
      "title": "Nix Module System Explained",
      "content_html": "<h1>Chapter 3</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<h2>Nix Module System Explained</h2>\n<!-- ![gruv3](images/gruv3.png) -->\n<p><img src=\"https://saylesss88.github.io/images/buildings1.png\" alt=\"buildings\" /></p>\n<p><strong>TL;DR</strong>: In this chapter, we will break down the Nix module system used by\nboth NixOS and Home-Manager. We will discuss using home-manager as a module and\nthe flexibility that modules give us. We will touch on options and break down\nthe <code>vim</code> module from the Nixpkgs collection. Finally we will display how to\ntest modules with the repl.</p>\n<p>Your <code>configuration.nix</code> is a module. For the Nixpkgs collection most modules\nare in <code>nixos/modules</code>.</p>\n<p>The suggested way of using <code>home-manager</code> according to their manual is as a\n<a href=\"https://nix-community.github.io/home-manager/index.xhtml#sec-install-nixos-module\">NixOS module</a>.\nBoth home-manager and NixOS use the same module system.</p>\n<h2>Module Structure</h2>\n<pre><code class=\"language-nix\">{\n  imports = [\n    # Paths to other modules.\n    # Compose this module out of smaller ones.\n  ];\n\n  options = {\n    # Option declarations.\n    # Declare what settings a user of this module can set.\n    # Usually this includes a global \"enable\" option which defaults to false.\n  };\n\n  config = {\n    # Option definitions.\n    # Define what other settings, services and resources should be active.\n    # Usually these depend on whether a user of this module chose to \"enable\" it\n    # using the \"option\" above.\n    # Options for modules imported in \"imports\" can be set here.\n  };\n}\n</code></pre>\n<p><code>imports</code>, <code>options</code>, and <code>config</code> are the top-level attributes of a Nix module.\nThey are the primary, reserved keys that the Nix module system recognizes and\nprocesses to combine different configurations into a single, cohesive system or\nuser environment. <code>config</code> is the same <code>config</code> you receive as a module argument\n(e.g. <code>{ pkgs, config, ... }:</code> at the top of your module function)</p>\n<p>Understanding <code>config</code>:</p>\n<p><code>config</code> is the big constantly updated blueprint of your entire system.</p>\n<p>Every time you bring in a new module, it adds its own settings and options to\nthis blueprint. So, when a module receives the <code>config</code> argument, it’s getting\nthe complete picture of everything you’ve asked NixOS to set up so far.</p>\n<p>This allows the module to:</p>\n<ul>\n<li>\n<p>See what other parts of your system are doing.</p>\n</li>\n<li>\n<p>Make smart decisions based on those settings.</p>\n</li>\n<li>\n<p>Add its own pieces to the overall plan, building on what’s already there.</p>\n</li>\n<li>\n<p>Most modules are functions that take an attribute set and return an attribute\nset.</p>\n</li>\n</ul>\n<p>To turn the above module into a function accepting an attribute set just add the\nfunction arguments to the top, click the eye to see the whole module:</p>\n<pre><code class=\"language-nix\">{ config, pkgs, ... }:\n~ {\n~   imports = [\n~     # Paths to other modules.\n~     # Compose this module out of smaller ones.\n~   ];\n~\n~   options = {\n~     # Option declarations.\n~     # Declare what settings a user of this module can set.\n~     # Usually this includes a global \"enable\" option which defaults to false.\n~   };\n~\n~   config = {\n~     # Option definitions.\n~     # Define what other settings, services and resources should be active.\n~     # Usually these depend on whether a user of this module chose to \"enable\" it\n~     # using the \"option\" above.\n~     # Options for modules imported in \"imports\" can be set here.\n~   };\n~ }\n</code></pre>\n<p>It may require the attribute set to contain:</p>\n<ul>\n<li>\n<p><code>config</code>: The configuration of the entire system.</p>\n</li>\n<li>\n<p><code>options</code>: All option declarations refined with all definition and declaration\nreferences.</p>\n</li>\n<li>\n<p><code>pkgs</code>: The attribute set extracted from the Nix package collection and\nenhanced with the <code>nixpkgs.config</code> option.</p>\n</li>\n<li>\n<p><code>modulesPath</code>: The location of the module directory of NixOS.</p>\n</li>\n</ul>\n<h2>Modularize your configuration.nix</h2>\n<p>Many people start of using a single <code>configuration.nix</code> and eventually their\nsingle file configuration gets too large to search through and maintain\nconveniently.</p>\n<p>This is where <strong>modules</strong> come in allowing you to break up your configuration\ninto logical parts. Your <code>boot.nix</code> will contain settings and options related to\nthe actual boot process. You’re <code>services.nix</code> will only have services and so\non…</p>\n<ul>\n<li>These modules are placed in a logical path relative to either your\n<code>configuration.nix</code> or equivalent or if you’re using flakes relative to your\n<code>flake.nix</code> or equivalent.\n<ul>\n<li>The <code>imports</code> mechanism takes paths to other modules as its argument and\ncombines them to be included in the evaluation of the system configuration.</li>\n</ul>\n</li>\n</ul>\n<blockquote>\n<pre><code class=\"language-nix\">{ ... }:\n{\n  imports = [\n     # Paths to other modules\n\n     # They can be relative paths\n     ./otherModule.nix\n\n     # Or absolute\n     /path/to/otherModule.nix\n\n     # Or to a directory\n     ../modules/home/shells/nushell\n  ];\n}\n</code></pre>\n</blockquote>\n<blockquote>\n<p>❗: The <strong>imports</strong> mechanism includes and evaluates the Nix expression found\nat the given path <em>as a module</em>. If that path is a directory, it will\nautomatically look for and evaluate a <code>default.nix</code> file within that directory\n<em>as a module</em>. It is common to have that <code>default.nix</code> be a function that only\nimports and combines all the modules in said directory. Like the above\nexample, in the nushell directory would be a <code>default.nix</code> that is\nautomatically imported and evaluated.</p>\n</blockquote>\n<p><strong>Crucial Distinction: <code>imports</code> vs. <code>import</code></strong>:</p>\n<p>Beginners often confuse the modules attribute <code>imports = [./module.nix]</code> here\nwith the Nix builtins function <code>import module.nix</code>. The first expects a path to\na file containing a NixOS module (having the same specific structure we’re\ndescribing here), while the second loads whatever Nix expression is in that file\n(no expected structure). –NixOS Wiki.</p>\n<p>Considering <code>configuration.nix</code> is a module, it can be imported like any other\nmodule and this is exactly what you do when getting started with flakes.</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  description = \"NixOS configuration\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n    home-manager.url = \"github:nix-community/home-manager\";\n    home-manager.inputs.nixpkgs.follows = \"nixpkgs\";\n  };\n\n  outputs = inputs@{ nixpkgs, home-manager, ... }: {\n    nixosConfigurations = {\n      hostname = nixpkgs.lib.nixosSystem {\n        system = \"x86_64-linux\";\n        modules = [\n          ./configuration.nix\n          home-manager.nixosModules.home-manager\n          {\n            home-manager.useGlobalPkgs = true;\n            home-manager.useUserPackages = true;\n            home-manager.users.jdoe = ./home.nix;\n\n            # Optionally, use home-manager.extraSpecialArgs to pass\n            # arguments to home.nix\n          }\n        ];\n      };\n    };\n  };\n}\n</code></pre>\n<p><code>modules = [...]</code> in <code>flake.nix</code>: This is effectively the initial <code>imports</code> list\nfor your entire NixOS system or Home Manager user configuration. It tells the\nNix module system: “Start by collecting and merging the configurations defined\nin these specific modules.”</p>\n<p>The above example is what you get from running:\n<code>nix flake new /etc/nixos -t github:nix-community/home-manager#nixos</code></p>\n<p>If you notice the <code>home-manager.nixosModules.home-manager</code>, that is what imports\nhome-manager as a module.</p>\n<p>You could also make the actual home-manager module and import it like this:</p>\n<pre><code class=\"language-nix\"># home-manager.nix\n{ inputs, outputs, ... }: {\n  imports = [\n    # Import home-manager's NixOS module\n    inputs.home-manager.nixosModules.home-manager\n  ];\n\n  home-manager = {\n    extraSpecialArgs = { inherit inputs outputs; };\n    users = {\n      # Import your home-manager configuration\n      your-username = import ../home-manager/home.nix;\n    };\n  };\n}\n</code></pre>\n<p>This “module” isn’t much different from the one included in the <code>flake.nix</code>\nabove, it is just shown here to show the flexibility of modules. They can be as\nbig and complex or as small and simple as you want. You can break up every\nsingle program or component of your configuration into individual modules or\nhave modules that bundle similar programs the choice is yours.</p>\n<p>Then in your <code>configuration.nix</code> or equivalent you would add <code>home-manager.nix</code>\nto your imports list and you would have home-manager as a NixOS module.</p>\n<details>\n<summary>\n✔️ Refresher (Click to Expand):\n</summary>\n<p>An <strong>attribute set</strong> is a collection of name-value pairs called <em>attributes</em>:</p>\n<p>Attribute sets are written enclosed in curly braces <code>{}</code>. Attribute names and\nattribute values are separated by an equal sign <code>=</code>. Each value can be an\narbitrary expression, terminated by a semicolon <code>;</code>.</p>\n<blockquote>\n<p><strong>Example</strong>:<a href=\"https://nix.dev/manual/nix/2.24/language/syntax#attrs-literal\">nix.dev reference</a>\nThis defines an attribute set with attributes named:</p>\n<ul>\n<li><code>x</code> with the value <code>123</code>, an integer</li>\n<li><code>text</code> with the value <code>\"Hello\"</code>, a string</li>\n<li><code>y</code> where the value is the result of applying the function <code>f</code> to the\nattribute set <code>{bla = 456; }</code></li>\n</ul>\n<pre><code class=\"language-nix\">{\n x = 123;\n text = \"Hello\";\n y = f { bla = 456; };\n}\n</code></pre>\n<pre><code class=\"language-nix\">{ a = \"Foo\"; b = \"Bar\"}.a\n~ \"Foo\"\n</code></pre>\n</blockquote>\n<p>Attributes can appear in any order. An attribute name may only occur once in\neach attribute set.</p>\n<blockquote>\n<p>❗ Remember <code>{}</code> is a valid attribute set in Nix.</p>\n</blockquote>\n<p>The following is a <strong>function</strong> with an attribute set argument, remember that\nanytime you see a <code>:</code> in Nix code it means this is a function. To the left is\nthe <strong>function arguments</strong> and to the right is the <strong>function body</strong>:</p>\n<pre><code class=\"language-nix\">{ a, b }: a + b\n</code></pre>\n<p>The simplest possible <strong>NixOS Module</strong>:</p>\n<pre><code class=\"language-nix\">{ ... }:\n{\n}\n</code></pre>\n</details>\n<p>NixOS produces a full system configuration by combining smaller, more isolated\nand reusable components: <strong>Modules</strong>. If you want to understand Nix and NixOS\nmake sure you grasp modules!</p>\n<p>A NixOS module defines configuration options and behaviors for system\ncomponents, allowing users to extend, customize, and compose configurations\ndeclaratively.</p>\n<p>A <strong>module</strong> is a file containing a Nix expression with a specific structure. It\n<em>declares</em> options for other modules to define (give a value). Modules were\nintroduced to allow extending NixOS without modifying its source code.</p>\n<p>To define any values, the module system first has to know which ones are\nallowed. This is done by declaring options that specify which attributes can be\nset and used elsewhere.</p>\n<p>If you want to write your own modules, I recommend setting up\n<a href=\"https://github.com/nix-community/nixd?tab=readme-ov-file\">nixd</a> or\n<a href=\"https://github.com/oxalica/nil\">nil</a> with your editor of choice. This will\nallow your editor to warn you about missing arguments and dependencies as well\nas syntax errors.</p>\n<h3>Declaring Options</h3>\n<p>Options are declared under the top-level <code>options</code> attribute with\n<code>lib.mkOption</code>.</p>\n<p><a href=\"https://nixos.org/manual/nixpkgs/stable/#function-library-lib.options.mkOption\">mkOption</a>\nCreates an Option attribute set. It accepts an attribute set with certain keys\nsuch as, <code>default</code>, <code>package</code>, and <code>example</code>.</p>\n<pre><code class=\"language-nix\"># options.nix\n{ lib, ... }:\n{\n  options = {\n    name = lib.mkOption { type = lib.types.str; };\n  };\n}\n</code></pre>\n<blockquote>\n<p><code>lib</code> provides helper functions from <code>nixpkgs.lib</code> and the ellipsis (<code>...</code>) is\nfor arbitrary arguments which means that this function is prepared to accept\n<strong>any additional arguments</strong> that the caller might provide, even if those\narguments are not explicitly named or used within the module’s body. They make\nthe modules more flexible, without the <code>...</code> each module would have to\nexplicitly list every possible argument it might receive, which would be\ncumbersome and error-prone. So <code>{lib, ... }:</code> means that “I need the <code>lib</code>\nargument” <strong>and</strong> I acknowledge that the module system might pass other\narguments automatically (like <code>config</code>, <code>pkgs</code>, etc.) and I’m fine with them\nbeing there, even if I don’t use them directly in this specific module file.</p>\n</blockquote>\n<h3>Defining Values</h3>\n<p>Options are <strong>set</strong> or <strong>defined</strong> under the top-level <code>config</code> attribute:</p>\n<pre><code class=\"language-nix\"># config.nix\n{ ... }:\n{\n  config = {\n    name = \"Slick Jones\";\n  };\n}\n</code></pre>\n<p>In this <strong>option declaration</strong>, we created an option <code>name</code> of type <em>string</em> and\nset that same option to a string.</p>\n<p><strong>Option Definitions</strong> can be in a separate file than <strong>Option Declarations</strong></p>\n<h3>Evaluating Modules</h3>\n<p>Modules are <strong>evaluated</strong> with\n<a href=\"https://nixos.org/manual/nixpkgs/stable/#module-system-lib-evalModules\">lib.evalModules</a>\n<code>lib.evalModules</code> evaluates a set of modules, typically once per application\n(e.g. once for NixOS and once for Home-Manager).</p>\n<h2>Checking out the Vim module provided by Nixpkgs</h2>\n<p>The following is <code>nixpkgs/nixos/modules/programs/vim.nix</code>, a module that is\nincluded in the Nixpkgs collection:</p>\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  pkgs,\n  ...\n}:\n\nlet\n  cfg = config.programs.vim;\nin\n{\n  options.programs.vim = {\n    enable = lib.mkEnableOption \"Vi IMproved, an advanced text\";\n\n    defaultEditor = lib.mkEnableOption \"vim as the default editor\";\n\n    package = lib.mkPackageOption pkgs \"vim\" { example = \"vim-full\"; };\n  };\n\n  # TODO: convert it into assert after 24.11 release\n  config = lib.mkIf (cfg.enable || cfg.defaultEditor) {\n    warnings = lib.mkIf (cfg.defaultEditor &amp;&amp; !cfg.enable) [\n      \"programs.vim.defaultEditor will only work if programs.vim.enable is\n       enabled, which will be enforced after the 24.11 release\"\n    ];\n    environment = {\n      systemPackages = [ cfg.package ];\n      variables.EDITOR = lib.mkIf cfg.defaultEditor (lib.mkOverride 900 \"vim\");\n      pathsToLink = [ \"/share/vim-plugins\" ];\n    };\n  };\n}\n</code></pre>\n<p>It provides options to enable Vim, set it as the default editor, and specify the\nVim package to use.</p>\n<details>\n<summary> ✔️ Breakdown of the vim module.(Click to Expand)</summary>\n1. Module Inputs and Structure:\n<pre><code class=\"language-nix\">{\n  config,\n  lib,\n  pkgs,\n  ...\n}\n</code></pre>\n<p><strong>Inputs</strong>: The module takes the above inputs and <code>...</code> (catch-all for other\nargs)</p>\n<ul>\n<li>\n<p><code>config</code>: Allows the module to read option values (e.g.\n<code>config.programs.vim.enable</code>). It provides access to the evaluated\nconfiguration.</p>\n</li>\n<li>\n<p><code>lib</code>: The Nixpkgs library, giving us helper functions like <code>mkEnableOption</code> ,\n<code>mkIf</code>, and <code>mkOverride</code>.</p>\n</li>\n<li>\n<p><code>pkgs</code>: The Nixpkgs package set, used to access packages like <code>pkgs.vim</code></p>\n</li>\n<li>\n<p><code>...</code>: Allows the module to accept additional arguments, making it flexible\nfor extension in the future.</p>\n</li>\n</ul>\n<blockquote>\n<p>Key Takeaways: A NixOS module is typically a function that can include\n<code>config</code>, <code>lib</code>, and <code>pkgs</code>, but it doesn’t require them. The <code>...</code> argument\nensures flexibility, allowing a module to accept extra inputs without breaking\nfuture compatibility. Using <code>lib</code> simplifies handling options (mkEnableOption,\nmkIf, mkOverride) and helps follow best practices. Modules define options,\nwhich users can set in their configuration, and <code>config</code>, which applies\nchanges based on those options.</p>\n</blockquote>\n<ol start=\"2\">\n<li>Local Configuration Reference:</li>\n</ol>\n<pre><code class=\"language-nix\">let\n  cfg = config.programs.vim;\nin\n</code></pre>\n<p>This is a local alias. Instead of typing <code>config.programs.vim</code> over and over,\nthe module uses <code>cfg</code>.</p>\n<ol start=\"3\">\n<li>Option Declaration</li>\n</ol>\n<pre><code class=\"language-nix\">options.programs.vim = {\n  enable = lib.mkEnableOption \"Vi IMproved, an advanced text\";\n  defaultEditor = lib.mkEnableOption \"vim as the default editor\";\n  package = lib.mkPackageOption pkgs \"vim\" { example = \"vim-full\"; };\n};\n</code></pre>\n<p>This defines three user-configurable options:</p>\n<ul>\n<li>\n<p><code>enable</code>: Turns on Vim support system-wide.</p>\n</li>\n<li>\n<p><code>defaultEditor</code>: Sets Vim as the system’s default <code>$EDITOR</code>.</p>\n</li>\n<li>\n<p><code>package</code>: lets the user override which Vim package is used.</p>\n</li>\n</ul>\n<blockquote>\n<p><code>mkPackageOption</code> is a helper that defines a package-typed option with a\ndefault (<code>pkgs.vim</code>) and provides docs + example. Using <code>lib.mkEnableOption</code>\nmakes it clear exactly where this function is coming from. Same with\n<code>lib.mkIf</code> and as you can see they can be further down the configuration,\nfurther from where you defined <code>with lib;</code> making it less clear where they\ncome from. Explicitness is your friend when it comes to reproducability and\nclarity.</p>\n</blockquote>\n<ol start=\"4\">\n<li>Conditional Configuration</li>\n</ol>\n<pre><code class=\"language-nix\">config = lib.mkIf (cfg.enable || cfg.defaultEditor) {\n</code></pre>\n<ul>\n<li>This block is only activated if <em>either</em> <code>programs.vim.enable</code> or\n<code>defaultEditor</code> is set.</li>\n</ul>\n<ol start=\"5\">\n<li>Warnings</li>\n</ol>\n<pre><code class=\"language-nix\">warnings = lib.mkIf (cfg.defaultEditor &amp;&amp; !cfg.enable) [\n  \"programs.vim.defaultEditor will only work if programs.vim.enable is enabled,\n   which will be enforced after the 24.11 release\"\n];\n</code></pre>\n<p>Gives you a soft warning if you try to set <code>defaultEditor = true</code> without also\nenabling Vim.</p>\n<ol start=\"6\">\n<li>Actual System Config Changes</li>\n</ol>\n<pre><code class=\"language-nix\">environment = {\n  systemPackages = [ cfg.package ];\n  variables.EDITOR = lib.mkIf cfg.defaultEditor (lib.mkOverride 900 \"vim\");\n  pathsToLink = [ \"/share/vim-plugins\" ];\n};\n</code></pre>\n<p>It adds Vim to your <code>systemPackages</code>, sets <code>$EDITOR</code> if <code>defaultEditor</code> is true,\nand makes <code>/share/vim-plugins</code> available in the environment.</p>\n</details>\n<p>The following is a bat home-manager module that I wrote:</p>\n<pre><code class=\"language-nix\"># bat.nix\n{\n  pkgs,\n  config,\n  lib,\n  ...\n}: let\n  cfg = config.custom.batModule;\nin {\n  options.custom.batModule.enable = lib.mkOption {\n    type = lib.types.bool;\n    default = false;\n    description = \"Enable bat module\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    programs.bat = {\n      enable = true;\n      themes = {\n        dracula = {\n          src = pkgs.fetchFromGitHub {\n            owner = \"dracula\";\n            repo = \"sublime\"; # Bat uses sublime syntax for its themes\n            rev = \"26c57ec282abcaa76e57e055f38432bd827ac34e\";\n            sha256 = \"019hfl4zbn4vm4154hh3bwk6hm7bdxbr1hdww83nabxwjn99ndhv\";\n          };\n          file = \"Dracula.tmTheme\";\n        };\n      };\n      extraPackages = with pkgs.bat-extras; [\n        batdiff\n        batman\n        prettybat\n        batgrep\n      ];\n    };\n  };\n}\n</code></pre>\n<p>Now I could add this to my <code>home.nix</code> to enable it:</p>\n<pre><code class=\"language-nix\"># home.nix\ncustom = {\n  batModule.enable = true;\n}\n</code></pre>\n<p>If I set this option to true the bat configuration is dropped in place. If it’s\nnot set to true, it won’t put the bat configuration in the system. Same as with\noptions defined in modules within the Nixpkgs repository.</p>\n<p>If I had set the default to <code>true</code>, it would automatically enable the module\nwithout requiring an explicit <code>custom.batModule.enable = true;</code> call in my\n<code>home.nix</code>.</p>\n<h3>Module Composition</h3>\n<p>NixOS achieves its full system configuration by combining the configurations\ndefined in various modules. This composition is primarily handled through the\n<code>imports</code> mechanism.</p>\n<p><code>imports</code>: This is a standard option within a NixOS or Home Manager\nconfiguration (often found in your configuration.nix or home.nix). It takes a\nlist of paths to other Nix modules. When you include a module in the imports\nlist, the options and configurations defined in that module become part of your\noverall system configuration.</p>\n<p>You declaratively state the desired state of your system by setting options\nacross various modules. The NixOS build system then evaluates and merges these\noption settings. The culmination of this process, which includes building the\nentire system closure, is represented by the derivation built by\n<code>config.system.build.toplevel</code>.</p>\n<h3>NixOS Modules and Dependency Locking with npins</h3>\n<details>\n<summary> ✔️ npins example (Click to Expand)</summary>\nAs our NixOS configurations grow in complexity, so too does the challenge of\nmanaging the dependencies they rely on. Ensuring consistency and reproducibility\nnot only applies to individual packages but also to the versions of Nixpkgs and\nother external resources our configurations depend upon.\n<p>Traditionally, NixOS configurations often implicitly rely on the version of\nNixpkgs available when <code>nixos-rebuild</code> is run. However, for more robust and\nreproducible setups, especially in collaborative environments or when rolling\nback to specific configurations, explicitly locking these dependencies to\nspecific versions becomes crucial.</p>\n<p>In the following example, we’ll explore how to use a tool called <code>npins</code> to\nmanage and lock the dependencies of a NixOS configuration, ensuring a more\npredictable and reproducible system. This will involve setting up a project\nstructure and using npins to pin the specific version of Nixpkgs our\nconfiguration relies on.</p>\n<p>This is the file structure:</p>\n<pre><code class=\"language-bash\">❯ tree\n.\n├── configuration.nix\n├── default.nix\n├── desktop.nix\n└── npins\n    ├── default.nix\n    └── sources.json\n</code></pre>\n<p>This uses <code>npins</code> for dependency locking. Install it and run this in the project</p>\n<p>directory:</p>\n<pre><code class=\"language-bash\">npins init\n</code></pre>\n<p>Create a <code>default.nix</code> with the following:</p>\n<pre><code class=\"language-nix\"># default.nix\n{ system ? builtins.currentSystem, sources ? import ./npins, }:\nlet\n  pkgs = import sources.nixpkgs {\n    config = { };\n    overlays = [ ];\n  };\n  inherit (pkgs) lib;\nin lib.makeScope pkgs.newScope (self: {\n\n  shell = pkgs.mkShell { packages = [ pkgs.npins self.myPackage ]; };\n\n    # inherit lib;\n\n  nixosSystem = import (sources.nixpkgs + \"/nixos\") {\n    configuration = ./configuration.nix;\n  };\n\n  moduleEvale = lib.evalModules {\n    modules = [\n      # ...\n    ];\n  };\n})\n</code></pre>\n<p>A <code>configuration.nix</code> with the following:</p>\n<pre><code class=\"language-nix\"># configuration.nix\n{\n  boot.loader.grub.device = \"nodev\";\n  fileSystems.\"/\".device = \"/devst\";\n  system.stateVersion = \"25.05\";\n\n  # declaring options means to declare a new option\n  # defining options means to define a value of an option\n  imports = [\n    # ./main.nix\n     ./desktop.nix # Files\n    # ./minimal.nix\n  ];\n\n  # mine.desktop.enable = true;\n}\n</code></pre>\n<p>And a <code>desktop.nix</code> with the following:</p>\n<pre><code class=\"language-nix\"># desktop.nix\n{ pkgs, lib, config, ... }:\n\n{\n  imports = [];\n\n  # Define an option to enable or disable desktop configuration\n  options.mine.desktop.enable = lib.mkEnableOption \"desktop settings\";\n\n  # Configuration that applies when the option is enabled\n  config = lib.mkIf config.mine.desktop.enable {\n    environment.systemPackages = [ pkgs.git ];\n  };\n}\n</code></pre>\n<p><code>mkEnableOption</code> defaults to false. Now in your <code>configuration.nix</code> you can\nuncomment <code>mine.desktop.enable = true;</code> to enable the desktop config and\nvice-versa.</p>\n<p>You can test that this works by running:</p>\n<pre><code class=\"language-bash\">nix-instantiate -A nixosSystem.system\n</code></pre>\n<p><code>nix-instantiate</code> performs only the evaluation phase of Nix expressions. During\nthis phase, Nix interprets the Nix code, resolves all dependencies, and\nconstructs derivations but does not execute any build actions. Useful for\ntesting.</p>\n<p>To check if this worked and <code>git</code> is installed in systemPackages you can load it\ninto <code>nix repl</code> but first you’ll want <code>lib</code> to be available so uncomment this in\nyour <code>default.nix</code>:</p>\n<pre><code class=\"language-nix\"># default.nix\ninherit lib;\n</code></pre>\n<p>Rerun <code>nix-instantiate -A nixosSystem.system</code></p>\n<p>Then load the repl and check that <code>git</code> is in <code>systemPackages</code>:</p>\n<pre><code class=\"language-bash\">nix repl -f .\nnix-repl&gt; builtins.filter (pkg: lib.hasPrefix \"git\" pkg.name) nixosSystem.config.environment.systemPackages\n</code></pre>\n<p>This shows the path to the derivation</p>\n<p>Check that mine.desktop.enable is true</p>\n<pre><code class=\"language-nix\">nix-repl&gt; nixosSystem.config.mine.desktop.enable\ntrue\n</code></pre>\n<p>As demonstrated with npins, explicitly managing the dependencies of your NixOS\nmodules is a powerful technique for ensuring the long-term stability and\nreproducibility of your system configurations. By pinning specific versions of\nNixpkgs and other resources, you gain greater control over your environment and\nreduce the risk of unexpected changes due to upstream updates.</p>\n</details>\n<h3>Best Practices</h3>\n<p>You’ll see the following all throughout Nix code and is convenient although it\ndoesn’t follow best practices. One reason is static analysis can’t reason about\nthe code (e.g. Because it implicitly brings all attributes into scope, tools\ncan’t verify which ones are actually being used), because it would have to\nactually evaluate the files to see which names are in scope:</p>\n<pre><code class=\"language-nix\"># utils.nix\n{ pkgs, ... }: {\n  environment.systemPackages = with pkgs; [\n    rustup\n    evcxr\n    nix-prefetch-git\n  ];\n}\n</code></pre>\n<p>Another reason the above expression is considered an “anti-pattern” is when more\nthen one <code>with</code> is used, it’s no longer clear where the names are coming from.</p>\n<p>Scoping rules for <code>with</code> are not intuitive, see\n<a href=\"https://github.com/NixOS/nix/issues/490\">issue</a> –nix.dev This can make\ndebugging harder, as searching for variable origins becomes ambiguous (i.e. open\nto more than one interpretation).</p>\n<p>The following follows best practices:</p>\n<pre><code class=\"language-nix\">{pkgs, ... }: {\n  environment.systemPackages = builtins.attrValues {\n    inherit (pkgs)\n      rustup\n      evcxr\n      nix-prefetch-git;\n  };\n}\n</code></pre>\n<ul>\n<li><a href=\"https://noogle.dev/f/builtins/attrValues\">Noogle builtins.attrValues</a></li>\n</ul>\n<details>\n<summary> ✔️ Above Command Summary (Click to Expand) </summary>\n<pre><code class=\"language-nix\">{\n  inherit (pkgs) rustup evcxr nix-prefetch-git;\n}\n</code></pre>\n<p>is equivalent to:</p>\n<pre><code class=\"language-nix\">{\n  rustup = pkgs.rustup;\n  evcxr = pkgs.evcxr;\n  nix-prefetch-git = pkgs.nix-prefetch-git;\n}\n</code></pre>\n<p>Applying <code>builtins.attrValues</code> produces:</p>\n<pre><code class=\"language-nix\">[ pkgs.evcxr pkgs.nix-prefetch-git pkgs.rustup ]\n</code></pre>\n<p>As you can see only the values are included in the list, not the keys. This is\nmore explicit and declarative but can be more complicated, especially for a\nbeginner.</p>\n<p><code>builtins.attrValues</code> returns the values of all attributes in the given set,\nsorted by attribute name. The above expression turns into something like the\nfollowing avoiding bringing every attribute name from <code>nixpkgs</code> into scope.</p>\n<p>A more straightforward example:</p>\n<pre><code class=\"language-nix\">attrValues {c = 3; a = 1; b = 2;}\n=&gt; [1 2 3]\n</code></pre>\n</details>\n<p>This approach avoids unintended name clashes or confusion when debugging.</p>\n<p>Upon looking into this a bit further, most people use the following format to\navoid the “anti-pattern” from using <code>with pkgs;</code>:</p>\n<pre><code class=\"language-nix\"># utils.nix\n{ pkgs, ... }: {\n  environment.systemPackages = [\n    pkgs.rustup\n    pkgs.evcxr\n    pkgs.nix-prefetch-git\n  ];\n}\n</code></pre>\n<p>While the performance differences might be negligible on modern computers,\nadopting this best practice from the start is highly recommended. The above\napproach is more explicit, it’s clear exactly where each package is coming from.</p>\n<p>If maintaining strict scope control matters, use <code>builtins.attrValues</code>.</p>\n<p>If readability and simplicity are more your priority, explicitly referencing\n<code>pkgs.&lt;packageName&gt;</code> might be better. Now you can choose for yourself.</p>\n<h4>Conclusion</h4>\n<p>As we have seen throughout this chapter, modules are the building blocks of your\nNixOS system and are themselves often functions. There are a few different ways\nto use these modules to build your system. In the next chapter,\n<a href=\"https://saylesss88.github.io/Nix_Flakes_Explained_4.html\">Nix Flakes Explained</a>\nwe will learn about Nix Flakes as a more modern and comprehensive entrypoint for\nmanaging your entire system and its dependencies.</p>\n<p>To further deepen your understanding of NixOS Modules and the broader ecosystem\nof tools and best practices surrounding them, the following resources offer\nvaluable insights and information.</p>\n<h4>Resources on Modules</h4>\n<details>\n<summary> ✔️ Resources (Click to Expand) </summary>\n<ul>\n<li>\n<p><a href=\"https://nixos.org/manual/nixos/stable/#sec-writing-modules\">WritingNixOsModules</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.wiki/wiki/NixOS_modules\">NixWikiNixOSModules</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/tutorials/module-system/a-basic-module/index.html\">nix.dev A basic module</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/tutorials/module-system/deep-dive#module-system-deep-dive\">ModuleSystemDeepDive</a></p>\n</li>\n<li>\n<p><a href=\"https://xeiaso.net/talks/asg-2023-nixos/\">xeiaso Nixos Modules for fun &amp; profit</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos-and-flakes.thiscute.world/other-usage-of-flakes/module-system\">NixOS Flakes Book Module System</a></p>\n</li>\n</ul>\n<h1>Videos</h1>\n<p><a href=\"https://www.youtube.com/watch?v=N7hFP_40DJo&amp;t=17s\">NixHour Writing NixOS modules</a>\n– This example is from this video\n<a href=\"https://infinisil.com/modules.mp4\">infinisilModules</a></p>\n<p><a href=\"https://www.youtube.com/watch?v=cZjOzOHb2ow\">tweagModuleSystemRecursion</a></p>\n</details>\n",
      "date_published": "2025-11-21T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Nix_Flakes_Explained_4.html",
      "url": "https://saylesss88.github.io/Nix_Flakes_Explained_4.html",
      "title": "Nix Flakes Explained",
      "content_html": "<h1>Chapter 4</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/trees3.cleaned.png\" alt=\"trees3\" /></p>\n<!-- <img src=\"https://saylesss88.github.io/images/gruv15.png\" width=\"800\" height=\"600\"> -->\n<h2>Nix Flakes Explained</h2>\n<p>If you’re completely new, take a look at\n<a href=\"https://nixos.wiki/wiki/flakes#Installing_flakes\">this</a> to get flakes on your\nsystem.</p>\n<p>For the Nix Flake man page type <code>man nix3 flake</code> and for a specific feature,\ntype something like <code>man nix3 flake-lock</code>.</p>\n<p>Flakes replace stateful channels (which cause much confusion among novices) and\nintroduce a more intuitive and consistent CLI, making them a perfect opportunity\nto start using Nix. – Alexander Bantyev\n<a href=\"https://serokell.io/blog/practical-nix-flakes\">Practical Nix Flakes</a></p>\n<p>The “state” being remembered and updated by channels is the specific revision of\nthe Nixpkgs repository that your local Nix installation considers “current” for\na given channel. When this state changes on your machine, your builds diverge\nfrom others whose machines have a different, independently updated channel\nstate.</p>\n<p>Channels are also constantly updated on the remote servers. So, “nixos-unstable”\ntoday refers to a different set of packages and versions than “nixos-unstable”\ndid yesterday or will tomorrow.</p>\n<p>Flakes solve this by making the exact revision of <code>nixpkgs</code> (and other\ndependencies) an explicit input within your <code>flake.nix</code> file, pinned in the\n<code>flake.lock</code>. This means the state is explicitly defined in the configuration\nitself, not implicitly managed by a global system setting.</p>\n<p>Evaluation time is notoriously slow on NixOS, the problem was that in the past\nNix evaluation wasn’t hermetic preventing effective evaluation caching. A <code>.nix</code>\nfile can import other Nix files or by looking them up in the Nix search path\n(<code>$NIX_PATH</code>). This causes a cached result to be inconsistent unless every file\nis perfectly kept track of. Flakes solve this problem by ensuring fully hermetic\nevaluation.</p>\n<p>“Hermetic” means that the output of an evaluation (the derivation itself)\ndepends <em>only</em> on the explicit inputs provided, not on anything external like\nenvironment variables or pulling in files only on your system. This is the\nproblem that Nix solves and the problem that flakes are built around.</p>\n<h2>What is a Nix Flake?</h2>\n<p><strong>Nix flakes</strong> are independent components in the Nix ecosystem. They define\ntheir own <strong>dependencies</strong> (inputs) and what they produce (outputs), which can\ninclude <strong>packages</strong>, <strong>deployment configurations</strong>, or <strong>Nix functions</strong> for\nother flakes to use.</p>\n<p>Flakes provide a standardized framework for building and managing software,\nmaking all project inputs explicit for greater reproducibility and\nself-containment.</p>\n<p>At its core, a flake is a source tree (like a Git repository) that contains a\n<code>flake.nix</code> file in its root directory. This file provides a standardized way to\naccess Nix artifacts such as packages and modules.</p>\n<p>Flakes provide a standard way to write Nix expressions (and therefore packages)\nwhose dependencies are version-pinned in a lock file, improving reproducibility\nof Nix installations. – NixOS Wiki</p>\n<p>Think of <code>flake.nix</code> as the central entry point of a flake. It not only defines\nwhat the flake produces but also declares its dependencies.</p>\n<h3>Key Concepts</h3>\n<p><code>flake.nix</code>: <strong>The Heart of a Flake</strong></p>\n<p>The <code>flake.nix</code> file is mandatory for any flake. It must contain an attribute\nset with at least one required attribute: <code>outputs</code>. It can also optionally\ninclude <code>description</code> and <code>inputs</code>.</p>\n<p><strong>Basic Structure:</strong></p>\n<pre><code class=\"language-nix\">{\n  description = \"Package description\";\n  inputs = { /* Dependencies go here */ };\n  outputs = { /* What the flake produces */ };\n  nixConfig = { /* Advanced configuration options */ };\n}\n</code></pre>\n<p>I typically see <code>nixConfig</code> used for extra-substituters for cachix. This is a\ngeneral-purpose way to define Nix configuration options that apply when this\nflake is evaluated or built. It ties into your <code>/etc/nix/nix.conf</code> or\n<code>~/.config/nix/nix.conf</code>.</p>\n<p>For example, create a directory and add a <code>flake.nix</code> with the following\ncontents, yes this is a complete <code>flake.nix</code> demonstrating <em>outputs</em> being the\nonly required attribute:</p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  outputs = _: { multiply = 2 * 2; };\n}\n</code></pre>\n<p>Now evaluate it with:</p>\n<pre><code class=\"language-bash\">nix eval .#multiply\n4\n</code></pre>\n<p>In the <code>outputs = _: { ... };</code> line, the <code>_</code> (underscore) is a placeholder\nargument. It represents the inputs that the outputs function could receive (like\n<code>inputs</code>, <code>self</code>, <code>pkgs</code>, etc.), but in this specific case, we’re not using any\nof them to define the multiply attribute. It’s a common convention in Nix to use\n<code>_</code> when an argument is required by a function but intentionally ignored.</p>\n<p>In the command <code>nix eval .#multiply</code>:</p>\n<ul>\n<li>\n<p>the <code>.</code> signifies the current directory, indicating that Nix should look for a\n<code>flake.nix</code> file in the directory where you’re running the command.</p>\n</li>\n<li>\n<p>The <code>#</code> is used to select a specific attribute from the <code>outputs</code> of the\nflake. In this case, it’s telling Nix to evaluate the <code>multiply</code> attribute.</p>\n</li>\n</ul>\n<p>In the next example we will create a <code>devShells</code> output as well as a <code>packages</code>\noutput.</p>\n<p><strong><code>flake.lock</code> auto-generated lock file</strong></p>\n<p>All flake inputs are pinned to specific revisions in a lockfile called\n<code>flake.lock</code> This file stores the revision info as JSON.</p>\n<p>The <code>flake.lock</code> file ensures that Nix flakes have purely deterministic outputs.\nA <code>flake.nix</code> file without an accompanying <code>flake.lock</code> should be considered\nincomplete and a kind of proto-flake. Any Nix CLI command that is run against\nthe flake—like <code>nix build</code>, <code>nix develop</code>, or even <code>nix flake show</code>—generates a\n<code>flake.lock</code> for you.</p>\n<p>Here’s an example section of a <code>flake.lock</code> file that pins Nixpkgs to a specific\nrevision:</p>\n<pre><code class=\"language-bash\">$ cat flake.lock\n{\n  \"nodes\": {\n    \"nixpkgs\": {\n      \"info\": {\n        \"lastModified\": 1587398327,\n        \"narHash\": \"sha256-mEKkeLgUrzAsdEaJ/1wdvYn0YZBAKEG3AN21koD2AgU=\"\n      },\n      \"locked\": {\n        \"owner\": \"NixOS\",\n        \"repo\": \"nixpkgs\",\n        \"rev\": \"5272327b81ed355bbed5659b8d303cf2979b6953\",\n        \"type\": \"github\"\n      },\n      \"original\": {\n        \"owner\": \"NixOS\",\n        \"ref\": \"nixos-20.03\",\n        \"repo\": \"nixpkgs\",\n        \"type\": \"github\"\n      }\n    },\n    \"root\": {\n      \"inputs\": {\n        \"nixpkgs\": \"nixpkgs\"\n      }\n    }\n  },\n  \"root\": \"root\",\n  \"version\": 5\n}\n</code></pre>\n<p>Any future build of this flake will use the version of <code>nixpkgs</code> recorded in the\nlock file. If you add new inputs, they will be automatically added when you run\na nix flake command like <code>nix flake show</code>. But it won’t replace existing locks.</p>\n<p>If you need to update a locked input to the latest version:</p>\n<pre><code class=\"language-bash\">nix flake lock --update-input nixpkgs\nnix build\n</code></pre>\n<p>The above command allows you to update individual inputs, and <code>nix flake update</code>\nwill update the whole lock file.</p>\n<h3>Helper functions that are good to know for working with Flakes</h3>\n<p><code>lib.genAttrs</code>: A function, given the name of the attribute, returns the\nattribute’s value</p>\n<p>Example:</p>\n<pre><code class=\"language-nix\">nix repl\nnix-repl&gt; :l &lt;nixpkgs&gt;\nnix-repl&gt; lib.genAttrs [ \"boom\" \"bash\" ] (name: \"sonic\" + name)\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-nix\">{\n  bash = \"sonicbash\";\n  boom = \"sonicboom\";\n}\n</code></pre>\n<p>You will often see the following:</p>\n<p>A common use for this with flakes is to have a list of different systems:</p>\n<pre><code class=\"language-nix\">     systems = [\n       \"x86_64-linux\"\n       \"aarch64-linux\"\n       \"x86_64-darwin\"\n       \"aarch64-darwin\"\n     ];\n</code></pre>\n<p>And use it to generate an attribute set for each listed system:</p>\n<pre><code class=\"language-nix\">eachSystem = lib.genAttrs systems;\n</code></pre>\n<p>The above command creates an attribute set by mapping over a list of system\nstrings. If you notice, you provide it a list (i.e. [ 1 2 3 ]) and the function\nreturns a set (i.e. <code>{ ... }</code>)</p>\n<p>Why <code>genAttrs</code> is useful:</p>\n<ul>\n<li>\n<p>It lets you define attributes (like <code>packages</code>, <code>checks</code>, <code>devShells</code>) per\nsupported system in a DRY(don’t repeat yourself), structured way.</p>\n</li>\n<li>\n<p><code>lib.mapAttrs</code>: A function, given an attribute’s name and value, returns a new\n<code>nameValuePair</code>.</p>\n</li>\n</ul>\n<p>Example:</p>\n<pre><code class=\"language-nix\">nix-repl&gt; builtins.mapAttrs (name: value: name + \"-\" + value) { x = \"foo\"; y = \"bar\"; }\n</code></pre>\n<p><strong>Output</strong>:</p>\n<pre><code class=\"language-nix\">{\n  x = \"x-foo\";\n  y = \"y-bar\";\n}\n</code></pre>\n<p><code>pkgs.mkShell</code>: is a specialized <code>stdenv.mkDerivation</code> that removes some\nrepetition when using it with <code>nix-shell</code> (or <code>nix develop</code>)</p>\n<p>Example:</p>\n<pre><code class=\"language-nix\">{ pkgs ? import &lt;nixpkgs&gt; {} }:\npkgs.mkShell {\n  packages = [ pkgs.gnumake ];\n\n  inputsFrom = [ pkgs.hello pkgs.gnutar ];\n\n  shellHook = ''\n    export DEBUG=1\n  '';\n}\n</code></pre>\n<h4>A Simple flake that outputs a devshell and a package</h4>\n<p>In a new directory create a <code>flake.nix</code></p>\n<pre><code class=\"language-nix\"># flake.nix\n{\n  outputs = {\n    self,\n    nixpkgs,\n  }: let\n    pkgs = nixpkgs.legacyPackages.x86_64-linux;\n  in {\n\n    packages.x86_64-linux.default = pkgs.kakoune; # You could define a meta-package here\n\n    devShells.x86_64-linux.default = pkgs.mkShell {\n      packages = [\n        pkgs.kakoune\n        pkgs.git\n        pkgs.ripgrep\n        pkgs.fzf\n      ];\n    };\n  };\n}\n</code></pre>\n<p><code>mkShell</code> is a wrapper around <code>mkDerivation</code></p>\n<p>This flake offers two main outputs for <code>x86_64-linux</code> systems:</p>\n<ol>\n<li>\n<p>A <strong>standard package</strong> (<code>packages.x86_64-linux.default</code>): This simple example\njust re-exports <code>kakoune</code> from <code>nixpkgs</code>. You could build your own apps here.</p>\n</li>\n<li>\n<p>A <strong>development shell</strong> (<code>devShells.x86_64-linux.default</code>): This provides a\nconvenient environment where you have specific tools available without\ninstalling them globally on your system.</p>\n</li>\n</ol>\n<p>To use this flake you have a few options:</p>\n<ul>\n<li>\n<p><code>nix run</code> will launch kakoune</p>\n</li>\n<li>\n<p><code>nix develop</code> will activate the development environment providing all of the\npkgs listed under <code>mkShell</code>.</p>\n</li>\n<li>\n<p>Or more explicitly <code>nix develop .#devShells.x86_64-linux.default</code>, does the\nsame thing as the command above.</p>\n</li>\n</ul>\n<h4>Flake References</h4>\n<details>\n<summary> ✔️ Flake References (Click to Expand) </summary>\n<p><strong>Flake references</strong> (flakerefs) are a way to specify the location of a flake.\nThey have two different formats:</p>\n<blockquote>\n<p><strong>Attribute set representation</strong>:</p>\n<pre><code class=\"language-nix\">{\n  type = \"github\";\n  owner = \"NixOS\";\n  repo = \"nixpkgs\";\n}\n</code></pre>\n<p>or <strong>URL-like syntax</strong>:</p>\n<pre><code class=\"language-nix\">github:NixOS/nixpkgs\n</code></pre>\n<p>These are used on the command line as a more convenient alternative to the\nattribute set representation. For instance, in the command</p>\n<pre><code class=\"language-nix\">nix build github:NixOS/nixpkgs#hello\n</code></pre>\n<p><code>github:NixOS/nixpkgs</code> is a flake reference (while <code>hello</code> is an output\nattribute). They are also allowed in the <code>inputs</code> attribute of a flake, e.g.</p>\n<pre><code class=\"language-nix\">inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";\n</code></pre>\n<p>is equivalent to</p>\n<pre><code class=\"language-nix\">inputs.nixpkgs = {\n  type = \"github\";\n  owner = \"NixOS\";\n  repo = \"nixpkgs\";\n};\n</code></pre>\n<p>–\n<a href=\"https://nix.dev/manual/nix/2.24/command-ref/new-cli/nix3-flake#flake-references\">nix.dev flake-references</a></p>\n</blockquote>\n</details>\n<h4>Nix Flake Commands</h4>\n<details>\n<summary> ✔️ Flake Commands (Click to Expand) </summary>\n<blockquote>\n<p><code>nix flake</code> provides subcommands for creating, modifying and querying <em>Nix\nFlakes</em>. Flakes are the unit for packaging Nix code in a reproducible and\ndiscoverable way. They can have dependencies on other flakes, making it\npossible to have multi-repository Nix projects.</p>\n</blockquote>\n<p>— From\n<a href=\"https://nix.dev/manual/nix/2.28/command-ref/new-cli/nix3-flake\">nix.dev Reference Manual</a></p>\n<ul>\n<li>\n<p>The main thing to note here is that <code>nix flake</code> is used to manage Nix flakes\nand that Flake commands are whitespace separated rather than hyphen <code>-</code>\nseparated.</p>\n</li>\n<li>\n<p>Flakes do provide some advantages when it comes to discoverability of outputs.</p>\n</li>\n<li>\n<p>For Example, two helpful commands to inspect a flake are:</p>\n<ul>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.28/command-ref/new-cli/nix3-flake-show\">nix flake show</a>\ncommand: Show the outputs provided by a flake.</p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.28/command-ref/new-cli/nix3-flake-check\">nix flake check</a>\ncommand: check whether the flake evaluates and run its tests.</p>\n</li>\n<li>\n<p>Any Nix CLI command that is run against a flake – like <code>nix build</code>,\n<code>nix develop</code>, <code>nix flake show</code> – generate a <code>flake.lock</code> file for you.</p>\n<ul>\n<li>The <code>flake.lock</code> file ensures that all flake inputs are pinned to specific\nrevisions and that Flakes have purely deterministic outputs.</li>\n</ul>\n</li>\n</ul>\n<p>Example:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">nix shell nixpkgs#ponysay --command ponysay \"Flakes Rock!\"\n</code></pre>\n<p>This works because of the [flake registry] that maps symbolic identifiers like\n<code>nixpkgs</code> to actual locations such as <code>https://github.com/NixOS/nixpkgs</code>. So the\nfollowing are equivalent:</p>\n<pre><code class=\"language-bash\">nix shell nixpkgs#ponysay --command ponysay Flakes Rock!\nnix shell github:NixOS/nixpkgs#ponysay --command ponysay Flakes Rock!\n</code></pre>\n<p>To override the <code>nixpkgs</code> registry with your own local copy you could:</p>\n<pre><code class=\"language-bash\">nix registry add nixpkgs ~/src/local-nixpkgs\n</code></pre>\n</details>\n<h3>Attribute Sets: The Building Blocks</h3>\n<details>\n<summary> ✔️ Attribute set Refresher (Click to Expand) </summary>\n<p><strong>Attribute sets</strong> are fundamental in Nix. They are simply collections of\nname-value pairs wrapped in curly braces <code>{}</code>.</p>\n<ul>\n<li>Example, (click to see Output):</li>\n</ul>\n<pre><code class=\"language-nix\">let\n  my_attrset = { foo = \"bar\"; };\nin\nmy_attrset.foo\n~ \"bar\"\n</code></pre>\n<p><strong>Top-Level Attributes of a Flake</strong>:</p>\n<p>Flakes have specific <strong>top-level attributes</strong> that can be accessed directly\n(without dot notation). The most common ones are <code>inputs</code>, <code>outputs</code>, and\n<code>nixConfig</code>.</p>\n  </details>\n<h3>Deeper Dive into the Structure of <code>flake.nix</code></h3>\n<!-- ![Flakes](images/Flakes.png) -->\n<p><code>inputs</code>: <strong>Declaring Dependencies</strong></p>\n<p>The <code>inputs</code> attribute set specifies the other flakes that your current flake\ndepends on.</p>\n<p>Each key in the <code>inputs</code> set is a name you choose for the dependency, and the\nvalue is a reference to that flake (usually a URL or a Git Repo).</p>\n<p>To access something from a dependency, you generally go through the <code>inputs</code>\nattribute (e.g., <code>inputs.helix.packages</code>).</p>\n<p>See\n<a href=\"https://saylesss88.github.io/flakes/flake_inputs_4.1.html\">Nix Flake inputs</a>\nfor a flake inputs deep dive.</p>\n<p><strong>Example:</strong> This declares dependencies on the <code>nixpkgs</code> and <code>import-cargo</code>\nflakes:</p>\n<pre><code class=\"language-nix\">inputs = {\n  import-cargo.url = \"github:edolstra/import-cargo\";\n  nixpkgs.url = \"nixpkgs\";\n};\n</code></pre>\n<p>When Nix evaluates your flake, it fetches and evaluates each input. These\nevaluated inputs are then passed as an attribute set to the outputs function,\nwith the keys matching the names you gave them in the inputs set.</p>\n<p>The special input <code>self</code> is a reference to the <code>outputs</code> and the source tree of\nthe current flake itself.</p>\n<p><strong><code>outputs</code>: Defining What Your Flake Provides</strong></p>\n<p>The <strong><code>outputs</code></strong> attribute defines what your flake makes available. This can\ninclude packages, NixOS modules, development environments (<code>devShells</code>) and\nother Nix derivations.</p>\n<p>Flakes can output arbitrary Nix values. However, certain outputs have specific\nmeanings for Nix commands and must adhere to particular types (often\nderivations, as described in the\n<a href=\"https://nixos.wiki/wiki/Flakes\">output schema</a>).</p>\n<p>You can inspect the outputs of a flake using the command:</p>\n<pre><code class=\"language-nix\">nix flake show\n</code></pre>\n<blockquote>\n<p>This command takes a flake URI and displays its outputs in a tree structure,\nshowing the attribute paths and their corresponding types.</p>\n</blockquote>\n<p><strong>Understanding the <code>outputs</code> Function</strong></p>\n<p>Beginners often mistakenly think that self and nixpkgs within\n<code>outputs = { self, nixpkgs, ... }: { ... }</code> are the outputs themselves. Instead,\nthey are the <em>input arguments</em> (often called <em>output arguments</em>) to the outputs\nfunction.</p>\n<p>The outputs function in <code>flake.nix</code> always takes a single argument, which is an\nattribute set. The syntax <code>{ self, nixpkgs, ... }</code> is Nix’s way of destructuring\nthis single input attribute set to extract the values associated with the keys\n<code>self</code> and <code>nixpkgs</code>.</p>\n<p>Flakes output your whole system configuration, packages, as well as Nix\nfunctions for use elsewhere.</p>\n<ul>\n<li>\n<p>For example, the <code>nixpkgs</code> repository has its own <code>flake.nix</code> file that\noutputs many helper functions via the <code>lib</code> attribute.</p>\n</li>\n<li>\n<p>For a deep dive into flake outputs, see\n<a href=\"https://saylesss88.github.io/flakes/flake_outputs_4.2.html\">Nix Flake Outputs</a></p>\n</li>\n</ul>\n<blockquote>\n<p>The <code>lib</code> convention The convention of using <code>lib</code> to output functions is\nobserved not just by Nixpkgs but by many other Nix projects. You’re free,\nhowever, to output functions via whichever attribute you prefer. –\n<a href=\"https://zero-to-nix.com/concepts/flakes/#inputs\">Zero to Nix Flakes</a></p>\n</blockquote>\n<p>Some flake outputs are required to be system specific (i.e. “x86_64-linux” for\n(64-bit AMD/Intel Linux) including packages, development environments, and NixOS\nconfigurations)</p>\n<p><strong>Variadic Attributes (…) and @-patterns</strong></p>\n<p>The <code>...</code> syntax in the input arguments of the outputs function indicates\nvariadic attributes, meaning the input attribute set can contain more attributes\nthan just those explicitly listed (like <code>lib</code> and <code>nixpkgs</code>).</p>\n<p><strong>Example:</strong></p>\n<pre><code class=\"language-nix\">mul = { a, b, ... }: a * b;\nmul { a = 3; b = 4; c = 2; } # 'c' is an extra attribute\n</code></pre>\n<p>However, you cannot directly access these extra attributes within the function\nbody unless you use the @-pattern:</p>\n<ul>\n<li>(Click for Output)</li>\n</ul>\n<pre><code class=\"language-nix\">mul = s@{ a, b, ... }: a  b  s.c; # 's' now refers to the entire input set\nmul { a = 3; b = 4; c = 2; } # Output: 24\n~ 24\n</code></pre>\n<p>When used in the outputs function argument list (e.g.,\n<code>outputs = { pkgs, ... } @ inputs)</code>, the @-pattern binds the entire input\nattribute set to a name (in this case, <code>inputs</code>) while also allowing you to\ndestructure specific attributes like pkgs.</p>\n<p><strong>What <code>outputs = { pkgs, ... } @ inputs: { ... };</code> does:</strong></p>\n<ol>\n<li>\n<p><strong>Destructuring:</strong> It tries to extract the value associated with the key\n<code>pkgs</code> from the input attribute set and binds it to the variable <code>pkgs</code>. The\n<code>...</code> allows for other keys in the input attribute set to be ignored during\nthis direct destructuring.</p>\n</li>\n<li>\n<p><strong>Binding the Entire Set:</strong> It binds the entire input attribute set to the\nvariable inputs.</p>\n<ul>\n<li>Example <code>flake.nix</code>:</li>\n</ul>\n</li>\n</ol>\n<pre><code class=\"language-nix\">{\ninputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\ninputs.home-manager.url = \"github:nix-community/home-manager\";\n\noutputs = { self, nixpkgs, ... } @ attrs: { # A `packages` output for the x86_64-linux platform\npackages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;\n\n    # A `nixosConfigurations` output (for a NixOS system named \"fnord\")\n    nixosConfigurations.fnord = nixpkgs.lib.nixosSystem {\n      system = \"x86_64-linux\";\n      specialArgs = attrs;\n      modules = [ ./configuration.nix ];\n    };\n\n};\n}\n</code></pre>\n<p><strong>Platform Specificity in Outputs</strong></p>\n<p>Flakes ensure that their outputs are consistent across different evaluation\nenvironments. Therefore, any package-related output must explicitly specify the\ntarget platform (a combination of architecture and OS, <code>x86_64-linux</code>).</p>\n<p><strong>legacyPackages Explained</strong></p>\n<p><code>legacyPackages</code> is a way for flakes to interact with the traditional, less\nstructured package organization of nixpkgs. Instead of packages being directly\nat the top level (e.g., <code>pkgs.hello</code>), <code>legacyPackages</code> provides a\nplatform-aware way to access them within the flake’s structured output format\n(e.g., <code>nixpkgs.legacyPackages.x86_64-linux.hello</code>). It acts as a bridge between\nthe flake’s expected output structure and nixpkgs’s historical organization.</p>\n<p><strong>The Sole Argument of outputs</strong></p>\n<p>It’s crucial to remember that the outputs function accepts only one argument,\nwhich is an attribute set. The <code>{ self, nixpkgs, ... }</code> syntax is simply\ndestructuring that single input attribute set.</p>\n<p><strong>Outputs of the Flake (Return Value)</strong></p>\n<p>The outputs of the flake refer to the attribute set that is returned by the\n<code>outputs</code> function. This attribute set can contain various named outputs like\n<code>packages</code>, <code>nixosConfigurations</code>, <code>devShells</code>, etc.</p>\n<p><strong>Imports: Including Other Nix Expressions</strong></p>\n<p>The <code>import</code> function in Nix is used to evaluate the Nix expression found at a\nspecified path (usually a file or directory) and return its value.</p>\n<p>Basic Usage: import <code>./path/to/file.nix</code></p>\n<p><strong>Passing Arguments During Import</strong></p>\n<p><code>import &lt;nixpkgs&gt; {}</code> is calling two functions, not one.</p>\n<ol>\n<li><code>import &lt;nixpkgs&gt;</code>: The first function call</li>\n</ol>\n<ul>\n<li>\n<p><code>import</code> is a built-in Nix function. Its job is to load and evaluate a Nix\nexpression from a specified path.</p>\n</li>\n<li>\n<p><code>&lt;nixpkgs&gt;</code> is a flake reference. When you use <code>import &lt;nixpkgs&gt;</code>, Nix\nevaluates the <code>default.nix</code> file (or sometimes <code>lib/default.nix</code>) found at\nthat location.</p>\n</li>\n<li>\n<p>The <code>default.nix</code> in <code>nixpkgs</code> evaluates to a function. This function is\ndesigned to be configurable, allowing you to pass arguments like <code>system</code>,\n<code>config</code>, etc. to customize how <code>nixpkgs</code> behaves and what packages it\nprovides.</p>\n</li>\n<li>\n<p>So, <code>import &lt;nixpkgs&gt;</code> doesn’t give you the <code>nixpkgs</code> package set directly; it\ngives you the function that generates the <code>nixpkgs</code> package set derivation.</p>\n</li>\n</ul>\n<ol start=\"2\">\n<li><code>{}</code>: The second function call (and its argument)</li>\n</ol>\n<ul>\n<li>\n<p><code>{}</code> denotes an empty attribute set</p>\n</li>\n<li>\n<p>When an attribute set immediately follows a function, it means you are calling\nthat function and passing the attribute set as its single argument.</p>\n</li>\n</ul>\n<p>So, the <code>{}</code> after <code>import &lt;nixpkgs&gt;</code> is not part of the <code>import</code> function\niteself. It’s the argument being passed to the function that <code>import &lt;nixpkgs&gt;</code>\njust returned.</p>\n<p>You can also pass an attribute set as an argument to the Nix expression being\nimported:</p>\n<pre><code class=\"language-nix\">let\nmyHelpers = import ./lib/my-helpers.nix { pkgs = nixpkgs; };\nin\n# ... use myHelpers\n</code></pre>\n<p>In this case, the Nix expression in <code>./lib/my-helpers.nix</code> is likely a function\nthat expects an argument (often named <code>pkgs</code> by convention):</p>\n<pre><code class=\"language-nix\"># ./lib/my-helpers.nix\n\n{ pkgs }:\nlet\nmyPackage = pkgs.stdenv.mkDerivation {\nname = \"my-package\"; # ...\n};\nin\nmyPackage\n</code></pre>\n<p>By passing <code>{ pkgs = nixpkgs; }</code> during the import, you are providing the\nnixpkgs value from your current <code>flake.nix</code> scope to the pkgs parameter expected\nby the code in <code>./lib/my-helpers.nix</code>.</p>\n<p><strong>Importing Directories (<code>default.nix</code>)</strong></p>\n<p>When you use import with a path that points to a directory, Nix automatically\nlooks for a file named <code>default.nix</code> within that directory. If found, Nix\nevaluates the expressions within <code>default.nix</code> as if you had specified its path\ndirectly in the import statement.</p>\n<ul>\n<li>For more advanced examples see\n<a href=\"https://saylesss88.github.io/flakes/flake_examples_4.3.html\">Nix Flake Examples</a></li>\n</ul>\n<h5>Conclusion: Unifying Your Nix Experience with Flakes</h5>\n<p>For some examples of more advanced outputs like <code>devShells</code> and <code>checks</code>, check\nout this blog post that I wrote:\n<a href=\"https://tsawyer87.github.io/posts/nix_flakes_tips/\">Nix Flakes Tips and Tricks</a></p>\n<p>In this chapter, we’ve explored Nix Flakes as a powerful and modern approach to\nmanaging Nix projects, from development environments to entire system\nconfigurations. We’ve seen how they provide structure, dependency management,\nand reproducibility through well-defined inputs and outputs. Flakes offer a\ncohesive way to organize your Nix code and share it with others.</p>\n<p>As we’ve worked with the flake.nix file, you’ve likely noticed its structure – a\ntop-level attribute set defining various outputs like devShells, packages,\nnixosConfigurations, and more. These top-level attributes are not arbitrary;\nthey follow certain conventions and play specific roles within the Flake\necosystem.</p>\n<p>In the next chapter,\n<a href=\"https://saylesss88.github.io/Understanding_Top-Level_Attributes_5.html\">Understanding Top-Level Attributes</a>\nwe will delve deeper into the meaning and purpose of these common top-level\nattributes. We’ll explore how they are structured, what kind of expressions they\ntypically contain, and how they contribute to the overall functionality and\norganization of your Nix Flakes. Understanding these attributes is key to\neffectively leveraging the full potential of Nix Flakes.</p>\n<h5>Further Resources</h5>\n<details>\n<summary> ✔️ Resources (Click to Expand)</summary>\n<ul>\n<li>\n<p><a href=\"https://serokell.io/blog/practical-nix-flakes\">practical-nix-flakes</a></p>\n</li>\n<li>\n<p><a href=\"https://xeiaso.net/blog/nix-flakes-1-2022-02-21/\">Nix Flakes an Introduction</a></p>\n</li>\n<li>\n<p><a href=\"https://www.tweag.io/blog/2020-07-31-nixos-flakes/\">tweag nix-flakes</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.wiki/wiki/Flakes\">NixOS-wiki Flakes</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/concepts/flakes.html\">nix.dev flakes</a></p>\n</li>\n<li>\n<p><a href=\"https://vtimofeenko.com/posts/practical-nix-flake-anatomy-a-guided-tour-of-flake.nix/\">anatomy-of-a-flake</a></p>\n</li>\n<li>\n<p><a href=\"https://jade.fyi/blog/flakes-arent-real/\">flakes-arent-real</a></p>\n</li>\n<li>\n<p><a href=\"https://mhwombat.codeberg.page/nix-book/#_attribute_set_operations\">wombats-book-of-nix</a></p>\n</li>\n<li>\n<p><a href=\"https://zero-to-nix.com/concepts/flakes/\">zero-to-nix flakes</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos-and-flakes.thiscute.world/\">nixos-and-flakes-book</a></p>\n</li>\n<li>\n<p><a href=\"https://flakehub.com/\">FlakeHub</a></p>\n</li>\n</ul>\n<p><img src=\"https://saylesss88.github.io/images/nixosnix.png\" alt=\"FlakeHub\" /></p>\n</details>\n",
      "date_published": "2025-11-21T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/Package_Definitions_Explained_6.html",
      "url": "https://saylesss88.github.io/Package_Definitions_Explained_6.html",
      "title": "Package Definitions Explained",
      "content_html": "<h1>Chapter 8</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<p><img src=\"https://saylesss88.github.io/images/coding2.png\" alt=\"coding2\" /></p>\n<!-- ![gruv1](images/gruv1.png) -->\n<h2>Package Definitions Explained</h2>\n<p>In Nix, the concept of a <strong>package</strong> can refer to two things:</p>\n<ul>\n<li>\n<p>A collection of files and data that constitute a piece of software or an\nartifact.</p>\n</li>\n<li>\n<p>A Nix <strong>expression</strong> that describes how to create such a collection. This\nexpression acts as a blueprint before the package exists in a tangible form.</p>\n</li>\n</ul>\n<p>The process begins with writing a <strong>package definition</strong> using the Nix language.\nThis definition contains the necessary instructions and metadata about the\nsoftware you intend to “package.”</p>\n<h2>The Journey from Definition to Package</h2>\n<details>\n<summary> ✔️ Click to Expand</summary>\n<ol>\n<li>\n<p><strong>Package Definition:</strong></p>\n<ul>\n<li>\n<p>This is essentially a function written in the Nix language.</p>\n</li>\n<li>\n<p>Nix language shares similarities with JSON but includes the crucial\naddition of functions.</p>\n</li>\n<li>\n<p>It acts as the blueprint for creating a package.</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Derivation:</strong></p>\n<ul>\n<li>\n<p>When the package definition is evaluated by Nix, it results in a\n<strong>derivation</strong>.</p>\n</li>\n<li>\n<p>A derivation is a concrete and detailed build plan.</p>\n</li>\n<li>\n<p>It outlines the exact steps Nix needs to take: fetching source code,\nbuilding dependencies, compiling code, and ultimately producing the\ndesired output (the package).</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Realization (Building the Package):</strong></p>\n<ul>\n<li>\n<p>You don’t get a pre-built “package” directly from the definition or the\nderivation.</p>\n</li>\n<li>\n<p>The package comes into being when Nix <strong>executes</strong> the derivation. This\nprocess is often referred to as “realizing” the derivation.</p>\n</li>\n</ul>\n</li>\n</ol>\n<p><strong>Analogy:</strong> Think of a package definition as an architectural blueprint, the\nderivation as the detailed construction plan, and the realized package as the\nfinished building.</p>\n</details>\n## Skeleton of a Derivation\n<p>The most basic derivation structure in Nix looks like this:</p>\n<pre><code class=\"language-nix\">{ stdenv }:\n\nstdenv.mkDerivation { }\n</code></pre>\n<ul>\n<li>\n<p>This is a function that expects an attribute set containing <code>stdenv</code> as its\nargument.</p>\n</li>\n<li>\n<p>It then calls <code>stdenv.mkDerivation</code> (a function provided by <code>stdenv</code>) to\nproduce a derivation.</p>\n</li>\n<li>\n<p>Currently, this derivation doesn’t specify any build steps or outputs.</p>\n</li>\n<li>\n<p>Further Reading:</p>\n</li>\n<li>\n<p><a href=\"https://ryantm.github.io/nixpkgs/stdenv/stdenv/\">The Standard Environment</a></p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/nix-pills/19-fundamentals-of-stdenv.html\">Fundamentals of Stdenv</a></p>\n</li>\n</ul>\n<h2>Example: A Simple “Hello” Package Definition</h2>\n<p>Here’s a package definition for the classic “hello” program:</p>\n<pre><code class=\"language-nix\"># hello.nix\n{\n  stdenv,\n  fetchzip,\n}:\n\nstdenv.mkDerivation {\n  pname = \"hello\";\n  version = \"2.12.1\";\n\n  src = fetchzip {\n    url = \"[https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz](https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz)\";\n    sha256 = \"\";\n  };\n}\n</code></pre>\n<ul>\n<li>\n<p>This is a Nix function that takes stdenv and fetchzip as arguments.</p>\n</li>\n<li>\n<p>It uses <code>stdenv.mkDerivation</code> to define the build process for the “hello”\npackage.</p>\n<ul>\n<li>\n<p><code>pname</code>: The package name.</p>\n</li>\n<li>\n<p><code>version</code>: The package version.</p>\n</li>\n<li>\n<p><code>src</code>: Specifies how to fetch the source code using <code>fetchzip</code>.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p><strong>Handling Dependencies: Importing Nixpkgs</strong></p>\n<ul>\n<li>\n<p>If you try to build <code>hello.nix</code> directly with <code>nix-build hello.nix</code>, it will\nfail because <code>stdenv</code> and <code>fetchzip</code> are part of Nixpkgs, which isn’t included\nin this isolated file.</p>\n</li>\n<li>\n<p>To make this package definition work, you need to pass the correct arguments\n(<code>stdenv</code>, <code>fetchzip</code>) to the function.</p>\n</li>\n</ul>\n<p>The recommended approach is to create a <code>default.nix</code> file in the same\ndirectory:</p>\n<pre><code class=\"language-nix\"># default.nix\n\nlet\n  nixpkgs = fetchTarball \"[https://github.com/NixOS/nixpkgs/tarball/nixos-24.05](https://github.com/NixOS/nixpkgs/tarball/nixos-24.05)\";\n  pkgs = import nixpkgs { config = {}; overlays = []; };\nin\n{\n  hello = pkgs.callPackage ./hello.nix { };\n}\n</code></pre>\n<ul>\n<li>\n<p>This <code>default.nix</code> imports Nixpkgs.</p>\n</li>\n<li>\n<p>It then uses <code>pkgs.callPackage</code> to call the function in <code>hello.nix</code>, passing\nthe necessary dependencies from Nixpkgs.</p>\n</li>\n<li>\n<p>You can now build the “hello” package using: <code>nix-build -A hello</code>. The <code>-A</code>\nflag tells Nix to build the attribute named hello from the top-level\nexpression in default.nix.</p>\n</li>\n</ul>\n<p><strong>Realizing the Derivation and Handling sha256</strong></p>\n<ul>\n<li>\n<p><strong>Evaluation vs. Realization</strong>: While “evaluate” refers to Nix processing an\nexpression, “realize” often specifically means building a derivation and\nproducing its output in the Nix store.</p>\n</li>\n<li>\n<p>When you first run <code>nix-build -A hello</code>, it will likely fail due to a missing\nsha256 hash for the source file. Nix needs this hash for security and\nreproducibility. The error message will provide the correct sha256 value.</p>\n</li>\n<li>\n<p><strong>Example Error</strong>:</p>\n</li>\n</ul>\n<pre><code class=\"language-bash\">  nix-build -A hello\n  error: hash mismatch in fixed-output derivation '/nix/store/pd2kiyfa0c06giparlhd1k31bvllypbb-source.drv':\n  specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n  got: sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=\n  error: 1 dependencies of derivation '/nix/store/b4mjwlv73nmiqgkdabsdjc4zq9gnma1l-hello-2.12.1.drv' failed to build\n</code></pre>\n<ul>\n<li>Replace the empty <code>sha256 = \"\";</code> in <code>hello.nix</code> with the provided correct\nvalue: <code>sha256 = \"1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=\";</code>.</li>\n</ul>\n<p><strong>Building and Running the Result</strong></p>\n<p>After updating the <code>sha256</code>, you can successfully build the package:</p>\n<pre><code class=\"language-bash\">nix-build -A hello\n</code></pre>\n<p>The output will be a result symlink pointing to the built package in the Nix\nstore. You can then run the “hello” program:</p>\n<pre><code class=\"language-bash\">./result/bin/hello\nHello, world!\n</code></pre>\n<h3>Swaytools Package Definition</h3>\n<p><strong>Example: The swaytools Package Definition</strong></p>\n<p>Let’s examine a more complex, real-world package definition from Nixpkgs:\n<code>nixpkgs/pkgs/tools/wayland/swaytools/default.nix</code>.</p>\n<pre><code class=\"language-nix\"># default.nix\n{\n  lib,\n  setuptools,\n  buildPythonApplication,\n  fetchFromGitHub,\n  slurp,\n}:\n\nbuildPythonApplication rec {\n  pname = \"swaytools\";\n  version = \"0.1.2\";\n\n  format = \"pyproject\";\n\n  src = fetchFromGitHub {\n    owner = \"tmccombs\";\n    repo = \"swaytools\";\n    rev = version;\n    sha256 = \"sha256-UoWK53B1DNmKwNLFwJW1ZEm9dwMOvQeO03+RoMl6M0Q=\";\n  };\n\n  nativeBuildInputs = [ setuptools ];\n\n  propagatedBuildInputs = [ slurp ];\n\n  meta = with lib; {\n    homepage = \"https://github.com/tmccombs/swaytools\";\n    description = \"Collection of simple tools for sway (and i3)\";\n    license = licenses.gpl3Only;\n    maintainers = with maintainers; [ atila ];\n    platforms = platforms.linux;\n  };\n}\n</code></pre>\n<h3>Breakdown of the Above default.nix</h3>\n<details>\n<summary>Click to Expand</summary>\n<p>1 <strong>Function Structure</strong>:</p>\n<ul>\n<li>\n<dl>\n<dt>The file starts with a function taking an attribute set of dependencies from\nNixpkgs: <code>{ lib, setuptools, buildPythonApplication, fetchFromGitHub, slurp }</code></dt>\n<dd>.</dd>\n</dl>\n</li>\n</ul>\n<ol start=\"2\">\n<li><strong>Derivation Creation</strong>:</li>\n</ol>\n<ul>\n<li>It calls <code>buildPythonApplication</code>, a specialized helper for Python packages\n(similar to <code>stdenv.mkDerivation</code> but pre-configured for Python). The <code>rec</code>\nkeyword allows attributes within the derivation to refer to each other.</li>\n</ul>\n<ol start=\"3\">\n<li><strong>Package Metadata</strong>:</li>\n</ol>\n<ul>\n<li>\n<p><code>pname</code> and <code>version</code> define the package’s name and version.</p>\n</li>\n<li>\n<p>The <code>meta</code> attribute provides standard package information like the homepage,\ndescription, license, maintainers, and supported platforms.</p>\n</li>\n</ul>\n<ol start=\"4\">\n<li><strong>Source Specification</strong>:</li>\n</ol>\n<ul>\n<li>The <code>src</code> attribute uses <code>fetchFromGitHub</code> to download the source code from\nthe specified repository and revision, along with its <code>sha256</code> hash for\nverification.</li>\n</ul>\n<ol start=\"5\">\n<li><strong>Build and Runtime Dependencies</strong>:</li>\n</ol>\n<ul>\n<li>\n<p><code>nativeBuildInputs</code>: Lists tools required during the build process (e.g.,\n<code>setuptools</code> for Python).</p>\n</li>\n<li>\n<p><code>propagatedBuildInputs</code>: Lists dependencies needed at runtime (e.g., <code>slurp</code>).</p>\n</li>\n</ul>\n<ol start=\"6\">\n<li><strong>Build Format</strong>:</li>\n</ol>\n<ul>\n<li><code>format = \"pyproject\";</code> indicates that the package uses a <code>pyproject.toml</code>\nfile for its Python build configuration.</li>\n</ul>\n<p><strong>Integration within Nixpkgs</strong></p>\n<ul>\n<li>\n<p><strong>Location</strong>: The swaytools definition resides in\n<code>pkgs/tools/wayland/swaytools/default.nix</code>.</p>\n</li>\n<li>\n<p><strong>Top-Level Inclusion</strong>: It’s made available as a top-level package in\n<code>pkgs/top-level/all-packages.nix</code> like this:</p>\n</li>\n</ul>\n<pre><code class=\"language-nix\"># all-packages.nix\nswaytools = python3Packages.callPackage ../tools/wayland/swaytools { };\n</code></pre>\n<ul>\n<li><code>python3Packages.callPackage</code> is used here because <code>swaytools</code> is a Python\npackage, and it ensures the necessary Python-related dependencies are correctly\npassed to the <code>swaytools</code> definition.</li>\n</ul>\n</details>\n<h2>Conclusion</h2>\n<p>In this chapter, we’ve journeyed through the fundamental concept of package\ndefinitions in Nix. We’ve seen how these Nix expressions act as blueprints,\nleading to the creation of derivations – the detailed plans for building\nsoftware. Finally, we touched upon the realization process where Nix executes\nthese derivations to produce tangible packages in the Nix store. Examining the\nsimple “hello” package and the more complex “swaytools” definition provided\npractical insights into the structure and key attributes involved in defining\nsoftware within the Nix ecosystem.</p>\n<p>The crucial step in this process, the transformation from a package definition\nto a concrete build plan, is embodied by the <strong>derivation</strong>. This detailed\nspecification outlines every step Nix needs to take to fetch sources, build\ndependencies, compile code, and produce the final package output. Understanding\nthe anatomy and lifecycle of a derivation is key to unlocking the full power and\nflexibility of Nix.</p>\n<p>In the <strong>next chapter</strong>,\n<a href=\"https://saylesss88.github.io/Intro_to_Nix_Derivations_7.html\">Introduction to Nix Derivations</a>,\nwe will delve deeper into the structure and components of these derivations. We\nwill explore the attributes that define a build process, how dependencies are\nmanaged within a derivation, and how Nix ensures the reproducibility and\nisolation of your software builds through this fundamental concept.</p>\n<h2>Resources</h2>\n<ul>\n<li><a href=\"https://nix.dev/tutorials/packaging-existing-software.html\">Packaging Existing Software</a></li>\n</ul>\n",
      "date_published": "2025-11-21T00:00:00+00:00",
      "author": {
        "name": "saylesss87@proton.me (saylesss88)"
      }
    },
    {
      "id": "https://saylesss88.github.io/README.html",
      "url": "https://saylesss88.github.io/README.html",
      "title": "Introduction",
      "content_html": "<p>📚 Welcome to nix-book!</p>\n<p><a href=\"https://github.com/saylesss88/nix-book/actions/workflows/deploy-book.yml\"><img src=\"https://github.com/saylesss88/nix-book/actions/workflows/deploy-book.yml/badge.svg?branch=main\" alt=\"Deploy mdBook to User GitHub Pages\" /></a></p>\n<p><a href=\"https://www.buymeacoffee.com/saylesss88\"><img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-%23FFDD00?style=for-the-badge&amp;logo=buy-me-a-coffee&amp;logoColor=black\" alt=\"Buy Me A Coffee\" /></a></p>\n<p>🚀 If you find this guide helpful, please consider leaving a star ⭐ or\nsupporting the project by buying me a coffee ☕. Your support helps keep this\ncontent updated and freely available.</p>\n<p>Follow nix-book with your preferred feed format for automatic notifications of\nnew content:</p>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/rss.xml\">nix-book RSS</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/atom.xml\">nix-book Atom</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/feed.json\">nix-book JSON Feed</a></p>\n</li>\n</ul>\n<p>Welcome to my personal journey and guide through the Nix ecosystem! This “book”\naims to be a practical and understandable resource for anyone looking to dive\ninto Nix, NixOS, and Home Manager. Whether you’re just getting started or\nlooking to deepen your understanding, I hope you find this resource helpful!</p>\n<p>I am a technologist with a wide range of interests that I’m passionate about,\nNixOS being one of them. I am also a privacy advocate trying to spread the word.</p>\n<p>✨ What You’ll Find Here</p>\n<p>This book covers a range of topics to help you harness the power of Nix:</p>\n<ul>\n<li>\n<p>Getting Started with the Nix Ecosystem: Covers the Nix Language, Package\nManager, and a minimal Btrfs-Subvol install with Disko and Flakes, including\nBtrfs Impermanence.</p>\n</li>\n<li>\n<p>Hardening NixOS: A deep dive into security and hardening practices with NixOS</p>\n</li>\n<li>\n<p>Hardening Networking: Configure dnscrypt-proxy, firewalls, and more.</p>\n</li>\n<li>\n<p>Gnupg and gpg-agent on NixOS</p>\n</li>\n<li>\n<p>Whonix KVM on NixOS</p>\n</li>\n<li>\n<p>NixOS as a Guest VM with Secureblue as the Host</p>\n</li>\n<li>\n<p>Version Control with Git</p>\n</li>\n<li>\n<p>Version Control with JJ</p>\n</li>\n<li>\n<p>Understanding Nix Functions: Explores practical Nix functions and their role\nwith NixOS Modules.</p>\n</li>\n<li>\n<p>NixOS Modules Explained: A dedicated deep dive into NixOS’s modular\nconfiguration system.</p>\n</li>\n<li>\n<p>Nix Flakes Explained: Comprehensive coverage of Flake Inputs, Outputs,\nExamples, and extending Flakes with Custom Packages using Overlays.</p>\n</li>\n<li>\n<p>Understanding Top-Level Attributes</p>\n</li>\n<li>\n<p>Package Definitions Explained</p>\n</li>\n<li>\n<p>Intro to Nix Derivations: Including how builders and Autotools work.</p>\n</li>\n<li>\n<p>Comparing Flakes and Traditional Nix</p>\n</li>\n<li>\n<p>Debugging and Tracing NixOS Modules</p>\n</li>\n<li>\n<p>Working with Nixpkgs Locally</p>\n</li>\n<li>\n<p>Fork, Clone, Contribute to Nixpkgs</p>\n</li>\n<li>\n<p>Nix Pull Requests</p>\n</li>\n<li>\n<p>Intro to Nushell on NixOS</p>\n</li>\n</ul>\n<p>My goal is to share my learnings and provide clear examples to make your Nix\nexperience smoother and more enjoyable.</p>\n<p>📖 How to Read the Book</p>\n<p>You can read the book directly here on GitHub by navigating through the folders,\nor for a more comfortable reading experience, check out the dedicated website. I\ntried to write it in a way where you could jump to the chapter you’re interested\nin and still be able to follow along:</p>\n<ul>\n<li>\n<p><a href=\"https://saylesss88.github.io/\">Start Here</a></p>\n</li>\n<li>\n<p>Browse the book chapters here on\n<a href=\"https://github.com/saylesss88/nix-book/tree/main/src\">GitHub</a>.</p>\n</li>\n</ul>\n<p>🙏 Contributions &amp; Feedback</p>\n<p>This book is a living document, and I welcome your input! If you find any\nerrors, have suggestions for improvements, or want to contribute a new section,\nplease feel free to:</p>\n<ul>\n<li>\n<p>Reach out to me on Reddit <code>u/saylesss88</code></p>\n</li>\n<li>\n<p>Open an Issue: For bug reports, typos, or content suggestions.</p>\n</li>\n<li>\n<p>Submit a Pull Request: If you have code changes or want to add content\ndirectly.</p>\n</li>\n</ul>\n<p>Your feedback is invaluable and helps make this resource better for everyone!</p>\n<p>⚖️ License</p>\n<p>This “Nix Book” is open-source and licensed under the Apache License 2.0.</p>\n<p>This means you are free to use, modify, and distribute this work, provided you\nadhere to the terms of the license. You can find the full text of the license in\nthe LICENSE file within this repository:</p>\n<p>To see a WIP book on privacy, checkout\n<a href=\"https://saylesss88.github.io/privacy-book/\">privacy-book</a></p>\n<p>View the\n<a href=\"https://github.com/saylesss88/nix-book/tree/main?tab=Apache-2.0-1-ov-file\">Apache License 2.0</a></p>\n"
    },
    {
      "id": "https://saylesss88.github.io/Getting_Started_with_Nix_1.html",
      "url": "https://saylesss88.github.io/Getting_Started_with_Nix_1.html",
      "title": "Chapter1",
      "content_html": "<h1>Chapter1</h1>\n<details>\n<summary> ✔️ Click to Expand Table of Contents</summary>\n<!-- toc -->\n</details>\n<!-- ![gruv13](images/gruv13.png) -->\n<p><img src=\"https://saylesss88.github.io/images/trees1.cleaned.png\" alt=\"trees\" /></p>\n<h2>Intro</h2>\n<p>Welcome to <em>nix-book</em>, an introductory book about Nix. This book leans more\ntowards using Flakes but will contrast traditional Nix where beneficial.\nOriginally, this content started as a blog. I’m refining its flow to make it\nmore cohesive.</p>\n<hr />\n<p>In this chapter, I will touch on the different parts of the Nix ecosystem, give\na quick example of each and explain how they fit together.</p>\n<ul>\n<li>Click <a href=\"https://saylesss88.github.io/rss.xml\">Here</a>, or the logo on the top\nright, next to print for the RSS feed.</li>\n</ul>\n<hr />\n<details>\n<summary>\n- ✔️: Will indicate an expandable section, click the little triangle to expand.\n</summary>\n<ul>\n<li>These sections are expandable!</li>\n</ul>\n</details>\n<p>The code blocks have an option to hide code, where I find it reasonable I will\nhide the outputs of the expressions. Click the eye in the right corner of the\ncode block next to the copy clipboard.</p>\n<p>Example hover over top-right corner of code block and click the eye to see\nhidden text:</p>\n<pre><code class=\"language-nix\">{\n  attrset = { a = 2; b = 4; };\n~  hidden_set = { a = hidden; b = set; };\n}\n</code></pre>\n<blockquote>\n<p>❗ If you’re new to Nix, think of it as a recipe book for software: you\ndescribe what you want (declarative), and Nix ensures it’s built the same way\nevery time (reproducible).</p>\n</blockquote>\n<h3>Why Learn Nix?</h3>\n<p>The main reason to learn Nix is that it allows you to write declarative scripts\nfor reproducible software builds. Rather than mutate the global state and\ninstall packages to a global location such as <code>/usr/bin</code> Nix stores packages in\nthe Nix store, usually the directory <code>/nix/store</code>, where each package has its\nown unique subdirectory. This paradigm gives you some powerful features, such\nas:</p>\n<ul>\n<li>\n<p>Allowing multiple versions or variants of the same package at the same time.\nThis prevents “DLL hell” from different applications having dependencies on\ndifferent versions of the same package.</p>\n</li>\n<li>\n<p>Atomic upgrades: Upgrading or uninstalling an application cannot break other\napplications and either succeed completely or fail completely preventing\npartial upgrades breaking your system. The nix store is immutable preventing\npackage management operations from overwriting other packages. They wouldn’t\noverwrite each other anyways because the hashing scheme ensures that new\nversions or repeat packages end up at different paths.</p>\n</li>\n<li>\n<p>Nix is designed to provide hermetic builds that aren’t affected by the\nenvironment, this helps you make sure that when packaging software that the\ndependencies are complete because they must be explicitly declared as inputs.\nWith other package managers it is more difficult to be sure that an\nenvironment variable or something in your <code>$PATH</code> isn’t affecting your build.</p>\n</li>\n</ul>\n<p>Let’s dive into the key characteristics of Nix:</p>\n<table><thead><tr><th>Concept</th><th>Description</th></tr></thead><tbody>\n<tr><td><strong>Pure</strong></td><td>Functions don’t cause side effects.</td></tr>\n<tr><td><strong>Functional</strong></td><td>Functions can be passed as arguments and returned as results.</td></tr>\n<tr><td><strong>Lazy</strong></td><td>Not evaluated until needed to complete a computation.</td></tr>\n<tr><td><strong>Declarative</strong></td><td>Describing a system outcome.</td></tr>\n<tr><td><strong>Reproducible</strong></td><td>Operations that are performed twice return same results</td></tr>\n</tbody></table>\n<blockquote>\n<p>❗ Important: In Nix, everything is an expression, there are no statements.</p>\n<p>❗ Important: Values in Nix are immutable.</p>\n</blockquote>\n<h3>The Nix Ecosystem</h3>\n<p>The <strong>Nix Language</strong> is the foundation of the ecosystem and is used to write\n<strong>Nix Expressions</strong>.</p>\n<p>Example:</p>\n<pre><code class=\"language-nix\">{ hello = \"world\"; }\n</code></pre>\n<h1>or</h1>\n<pre><code class=\"language-nix\">\"foo\" + \"bar\"\n</code></pre>\n<p>While the Nix language provides the foundation for writing expressions, it is\nonly part of the ecosystem. These expressions become powerful when used within\nthe Nix Package Manager, which evaluates and realizes them into tangible\nsoftware builds and system configurations. This is where Nixpkgs and NixOS come\ninto play.</p>\n<h3>The Nix Package Manager, Nixpkgs, and NixOS</h3>\n<p>At the heart of the Nix ecosystem is <strong>Nix Package Manager</strong>. This powerful\nengine is responsible for orchestrating the entire process: taking <strong>Nix\nexpressions</strong> (like <em>package definitions</em> and <em>configuration modules</em>),\nevaluating them into precise <em>derivations</em>, executing their build steps (the\n<em>realization phase</em>), and meticulously managing the immutable Nix store.</p>\n<p>A cornerstone of the Nix ecosystem is <strong>Nixpkgs</strong>. This vast collection\ncomprises tens of thousands of Nix expressions that describe how to build a wide\narray of software packages from source. Nixpkgs is more than just a package\nrepository—it also contains <strong>NixOS Modules</strong>, declarative configurations that\ndefine system behavior, ensuring a structured and reproducible environment.\nThese modules enable users to declaratively describe a Linux system, with each\nmodule contributing to the desired state of the overall system by leveraging\n<em>package definitions</em> and <em>derivations</em>. This is how NixOS emerges: it is quite\nsimply the natural consequence of applying the Nix philosophy to building an\nentire Linux operating system.</p>\n<p>We will further expand our understanding of modules in\n<a href=\"https://saylesss88.github.io/NixOS_Modules_Explained_3.html\">Chapter 3</a></p>\n<p>The following is an example of a NixOS module that is part of the <code>nixpkgs</code>\ncollection:</p>\n<pre><code class=\"language-nix\"># nixpkgs/nixos/modules/programs/zmap.nix\n{\n  pkgs,\n  config,\n  lib,\n  ...\n}:\n\nlet\n  cfg = config.programs.zmap;\nin\n{\n  options.programs.zmap = {\n    enable = lib.mkEnableOption \"ZMap, a network scanner designed for Internet-wide network surveys\";\n  };\n\n  config = lib.mkIf cfg.enable {\n    environment.systemPackages = [ pkgs.zmap ];\n\n    environment.etc.\"zmap/blacklist.conf\".source = \"${pkgs.zmap}/etc/zmap/blacklist.conf\";\n    environment.etc.\"zmap/zmap.conf\".source = \"${pkgs.zmap}/etc/zmap.conf\";\n  };\n}\n</code></pre>\n<ul>\n<li>This module, <code>programs.zmap.nix</code>, demonstrates how NixOS configurations work.\nIt defines an enable option for the ZMap network scanner. If enabled by the\nuser in their system configuration, the module ensures the <code>zmap</code> package is\ninstalled and its default configuration files are placed in <code>/etc</code>, allowing\nZMap to be managed declaratively as part of the operating system.\n<ul>\n<li>\n<p>When <code>nixpkgs</code> is imported (e.g., in a NixOS configuration), the\nconfiguration options and settings defined by its modules (like\n<code>programs.zmap.nix</code>) become available for use, typically accessed via dot\nnotation (e.g., <code>config.programs.zmap.enable</code>). This ability to make such a\nhuge set of modules and packages readily available without a significant\nperformance penalty is due to Nix’s <strong>lazy evaluation</strong>; only the\nexpressions required for a particular build or configuration are actually\nevaluated.</p>\n</li>\n<li>\n<p>Most of the time you’ll simply <a href=\"https://search.nixos.org/packages\">search</a>\nto see if the package is already included in <code>nixpkgs</code> and follow the\ninstructions there to get it on your system. It is good practice to first\nsearch for the <a href=\"https://search.nixos.org/options?\">options</a> to see what\nconfigurable settings are available, and then proceed to search for the\npackage itself if you know it exists or if you need its specific package\ndefinition. When you look up the options for Zmap, <code>programs.zmap.enable</code> is\nall that is listed in this example.</p>\n</li>\n<li>\n<p>Home Manager uses the same underlying Nix module system as NixOS, and when\nyou do something like home.packages = with pkgs; you are referring to the\nsame package derivations from nixpkgs as you would with\n<code>environment.systemPackages</code>. However, Home Manager’s own configuration\nmodules (e.g., for <code>programs.zsh</code> or <code>git</code>) are distinct and reside in the\nHome Manager repository, designed for user-specific configurations.</p>\n</li>\n</ul>\n</li>\n</ul>\n<p>One of the main differentiating aspects of Nix, as opposed to traditional\npackage managers, is this concept that package builds are treated as pure\nfunctions. This functional paradigm ensures consistency and reproducibility,\nwhich are core tenets of the Nix philosophy.</p>\n<p><img src=\"https://saylesss88.github.io/images/nix_isnot_nixos.png\" alt=\"Nix is not\" /></p>\n<p><strong>Fig. X:</strong> Conceptual diagram illustrating the distinction between Nix and\nNixOS. Source: xeiaso, from the blog post “Making NixOS modules for fun and\n(hopefully) profit”, <a href=\"https://xeiaso.net/talks/asg-2023-nixos/\">https://xeiaso.net/talks/asg-2023-nixos/</a>.</p>\n<p>Nix expressions permeate the ecosystem—everything in Nix is an expression,\nincluding the next key components: package definitions and derivations.</p>\n<h3>Package Definitions &amp; Derivations</h3>\n<p><strong>Package Definitions</strong> are specialized expressions that tell Nix how to build\nsoftware.</p>\n<p>Example package definition:</p>\n<pre><code class=\"language-nix\"># hello.nix\n {pkgs ? import &lt;nixpkgs&gt; {}}:\n pkgs.stdenv.mkDerivation {\n  pname = \"hello\";\n  version = \"2.12.1\";\n\n  src = pkgs.fetchurl {\n    url = \"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz\";\n    sha256 = \"086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd\";\n  };\n\n  nativeBuildInputs = [pkgs.autoconf pkgs.automake pkgs.gcc];\n\n  configurePhase = ''\n    ./configure --prefix=$out\n  '';\n\n  buildPhase = ''\n    make\n  '';\n\n  installPhase = ''\n    make install\n  '';\n }\n</code></pre>\n<ol>\n<li><strong>Evaluation Phase</strong>:</li>\n</ol>\n<p>Now when you run something like:</p>\n<pre><code class=\"language-bash\">nix-instantiate hello.nix\nwarning: you did not specify '--add-root'; the result might be removed by the garbage collector\n/nix/store/p2hbg16a9kpqgx2nzcsq39wmnyxyq4jy-hello-2.12.1.drv\n</code></pre>\n<ul>\n<li>Nix evaluates the expression and produces a <code>.drv</code> file (the <strong>derivation</strong>),\na precise JSON-like blueprint describing how the package will be built. It\ndoes not contain the built software itself.</li>\n</ul>\n<ol start=\"2\">\n<li><strong>Realization Phase</strong>:</li>\n</ol>\n<p>When you run:</p>\n<pre><code class=\"language-bash\">nix-build hello.nix\n#...snip...\nshrinking RPATHs of ELF executables and libraries in /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1\nshrinking /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1/bin/hello\nchecking for references to /build/ in /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1...\ngzipping man pages under /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1/share/man/\npatching script interpreter paths in /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1\nstripping (with command strip and flags -S -p) in  /nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1/bin\n/nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1\n</code></pre>\n<ul>\n<li>\n<p>Nix realizes the derivation by actually executing the build steps, fetching\nsources, compiling (if needed), and producing the final result (typically\nstored in e.g. <code>/nix/store/53hqyw72dijq3wb5kc0ln04g681gk6cp-hello-2.12.1</code>)</p>\n</li>\n<li>\n<p><code>nix-build</code> also creates a symlink named <code>result</code> in your current directory,\npointing to the final build output in the Nix store.</p>\n</li>\n</ul>\n<ol start=\"3\">\n<li>Execute the program:</li>\n</ol>\n<pre><code class=\"language-bash\">./result/bin/hello\nHello, world!\n</code></pre>\n<p><code>result/bin/hello</code> points to the executable inside the output of the\nderivation.The derivation describes how the package is built, but does not\ninclude the final binaries.</p>\n<p>To say that another way, the derivation is not the executable. The executable is\none of the derivations <code>outputs</code>. When Nix “realizes” a derivation, it executes\nthose build instructions, and the result is the actual built software, which\ngets placed into its own unique path in the Nix store.</p>\n<p>A single derivation can produce multiple outputs. The executable is typically\npart of the <code>out</code> output, specifically in its <code>bin</code> directory.</p>\n<p>Here is a small snippet of what a <code>.drv</code> file could look like, I got this from\nbuilding the hello derivation and running the following on the store path:</p>\n<pre><code class=\"language-bash\">nix show-derivation /nix/store/9na8mwp5zaprikqaqw78v6cdn1rxac7i-hello-2.12.1\n</code></pre>\n<pre><code class=\"language-nix\">{\n  \"/nix/store/871398c9cbskmzy6bvfnynr8yrlh7nk0-hello-2.12.1.drv\": {\n    \"args\": [\n      \"-e\",\n      \"/nix/store/v6x3cs394jgqfbi0a42pam708flxaphh-default-builder.sh\"\n    ],\n    \"builder\": \"/nix/store/1jzhbwq5rjjaqa75z88ws2b424vh7m53-bash-5.2p32/bin/bash\",\n    \"env\": {\n      \"__structuredAttrs\": \"\",\n      \"buildInputs\": \"\",\n      \"builder\": \"/nix/store/1jzhbwq5rjjaqa75z88ws2b424vh7m53-bash-5.2p32/bin/bash\",\n      \"cmakeFlags\": \"\",\n      \"configureFlags\": \"\",\n      \"depsBuildBuild\": \"\",\n      \"depsBuildBuildPropagated\": \"\",\n      \"depsBuildTarget\": \"\",\n      \"depsBuildTargetPropagated\": \"\",\n      \"depsHostHost\": \"\",\n      \"depsHostHostPropagated\": \"\",\n      \"depsTargetTarget\": \"\",\n      \"depsTargetTargetPropagated\": \"\",\n      \"doCheck\": \"\",\n      \"doInstallCheck\": \"\",\n      \"mesonFlags\": \"\",\n      \"name\": \"hello-2.12.1\",\n      \"nativeBuildInputs\": \"\",\n      \"out\": \"/nix/store/9na8mwp5zaprikqaqw78v6cdn1rxac7i-hello-2.12.1\",\n      \"outputs\": \"out\",\n      \"patches\": \"\",\n      \"pname\": \"hello\",\n      \"propagatedBuildInputs\": \"\",\n      \"propagatedNativeBuildInputs\": \"\",\n      \"src\": \"/nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz\",\n      \"stdenv\": \"/nix/store/80wijs24wjp619zmrasrh805bax02xjm-stdenv-linux\",\n      \"strictDeps\": \"\",\n      \"system\": \"x86_64-linux\",\n      \"version\": \"2.12.1\"\n    },\n# ... snip ...\n</code></pre>\n<h4>Conclusion</h4>\n<p>In this introductory chapter, we’ve laid the groundwork for understanding the\npowerful Nix ecosystem. We explored how the Nix Language forms the declarative\nbedrock, enabling us to define desired system states and software builds as\nexpressions. You saw how the Nix Package Manager orchestrates this process,\ntransforming those expressions into precise derivations during the evaluation\nphase, and then faithfully “realizing” them into reproducible, isolated\nartifacts within the immutable <code>/nix/store</code>.</p>\n<p>We also introduced the vast Nixpkgs collection, which provides tens of thousands\nof package definitions and forms the foundation for NixOS — a fully declarative\noperating system built on these principles—and even user-level configurations\nlike those managed by Home Manager. This unique functional approach, with its\nemphasis on immutability and lazy evaluation, is what enables Nix’s promises of\nconsistency, atomic upgrades, and truly hermetic builds, fundamentally changing\nhow we think about software and system management.</p>\n<h5>Related Sub-Chapters</h5>\n<ul>\n<li>\n<p>The <a href=\"https://saylesss88.github.io/nix/nix_language.html\">Nix Language</a></p>\n</li>\n<li>\n<p><a href=\"https://saylesss88.github.io/nix/nix_package_manager.html\">Nix Package Manager</a></p>\n</li>\n</ul>\n<p>Now that you have a foundational understanding of the Nix ecosystem and its core\noperational cycle, we are ready to delve deeper into the building blocks of Nix\nexpressions. In the next chapter,\n<a href=\"https://saylesss88.github.io/Understanding_Nix_Functions_2.html\">Understanding Nix Functions</a>,\nwe will peel back the layers and explore the intricacies of function arguments,\nadvanced patterns, scope, and how functions play a crucial role in building more\nsophisticated Nix expressions and derivations.</p>\n<p>Here are some resources that are helpful for getting started:</p>\n<h4>Resources</h4>\n<details>\n<summary> ✔️ Resources (Click to Expand)</summary>\n<ul>\n<li>\n<p><a href=\"https://search.nixos.org/packages\">NixOS Search</a></p>\n</li>\n<li>\n<p><a href=\"https://search.nixos.org/options?\">NixOS Options</a></p>\n</li>\n<li>\n<p><a href=\"https://home-manager-options.extranix.com/?query=&amp;release=master\">Extranix Home-Manager Option Search</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/nix-community/awesome-nix\">awesome-nix</a></p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/Nix_ecosystem\">Nix Core Ecosystem</a>, Nix, NixOS,\nNix Lang, Nixpkgs are all distinctly different; related things which can be\nconfusing for beginners this article explains them.</p>\n</li>\n<li>\n<p><a href=\"https://github.com/nixos/nixpkgs\">nixpkgs</a>: Vast package repository</p>\n</li>\n<li>\n<p><a href=\"https://nixos.org/guides/how-nix-works/\">How Nix Works</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/manual/nix/2.26/language/types#type-attrs\">Nix Reference Manual Data Types</a>\nThe main Data Types you’ll come across in the Nix ecosystem</p>\n</li>\n<li>\n<p><a href=\"https://wiki.nixos.org/wiki/NixOS_Wiki\">NixOS Wiki</a></p>\n</li>\n<li>\n<p><a href=\"https://nix.dev/\">nix.dev</a>: Has become the top respected source of\ninformation in my opinion. There is a lot of great stuff in here, and they\nactively update the information.</p>\n</li>\n</ul>\n</details>\n<pre><code>`````nix repl\n(let a = \"2\"; in                   # Let expressions are a way to create variables\na + a + builtins.toString \"4\")\n`````\n</code></pre>\n"
    }
  ]
}