{"id":5327,"date":"2026-03-29T23:54:18","date_gmt":"2026-03-30T06:54:18","guid":{"rendered":"https:\/\/catbradley.io\/?p=5327"},"modified":"2026-03-29T23:54:18","modified_gmt":"2026-03-30T06:54:18","slug":"how-i-add-tools-to-my-immutable-linux-without-rebooting","status":"publish","type":"post","link":"https:\/\/catbradley.io\/?p=5327","title":{"rendered":"How I Add Tools to My Immutable Linux Without Rebooting"},"content":{"rendered":"<p>If you\u2019ve recently dipped your toes into the world of <a href=\"https:\/\/itsfoss.com\/immutable-linux-distros\/\" rel=\"noreferrer\">immutable Linux distributions<\/a> like <a href=\"https:\/\/fedoraproject.org\/atomic-desktops\/silverblue\/\" rel=\"noopener\">Fedora Silverblue<\/a>, <a href=\"https:\/\/get.opensuse.org\/microos\/\" rel=\"noopener\">openSUSE MicroOS<\/a>, or even the Steam Deck, you&#8217;ll encounter this issue eventually. <\/p>\n<p>You try to perform a basic task, like adding a custom script to <code>\/usr\/bin<\/code> or creating a global configuration directory, and the terminal throws an error: <strong>Read-only file system.<\/strong><\/p>\n<p>It\u2019s a frustrating moment. You chose an <a href=\"https:\/\/itsfoss.com\/immutable-distro\/\" rel=\"noreferrer\">immutable OS <\/a>for the stability, the atomic updates, and the &#8220;unbreakable&#8221; nature of the system. But now you feel like a guest in your own house.<\/p>\n<p>The traditional fixes, manually mounting an overlay filesystem or using rpm-ostree to layer packages, either require a reboot or complex manual management.<\/p>\n<p><a href=\"https:\/\/www.freedesktop.org\/software\/systemd\/man\/latest\/systemd-sysext.html\">systemd-sysext<\/a> was built specifically to solve this problem. This often-overlooked utility uses OverlayFS under the hood but adds compatibility checking, systemd integration, and a standardized format, allowing you to dynamically merge binaries and libraries into <code>\/usr<\/code> at runtime, <strong>without touching the underlying read-only image and without a reboot<\/strong>.<\/p>\n<h2>Quick Look at Immutability<\/h2>\n<p>To understand why we need <code>sysext<\/code>, you first have to understand why the Linux world is moving toward immutability. In a traditional &#8220;mutable&#8221; distribution like <a href=\"https:\/\/ubuntu.com\/\" rel=\"noreferrer\">Ubuntu <\/a>or <a href=\"https:\/\/archlinux.org\/\" rel=\"noreferrer\">Arch<\/a>, the root filesystem is a giant, writable scratchpad. Any process with root privileges can modify any file in <code>\/usr<\/code> or <code>\/bin<\/code>. <\/p>\n<p>While this gives us total freedom, it\u2019s also a major source of system drift. Over time, manual changes, conflicting libraries, and failed package installations make the system unpredictable.<\/p>\n<p>Immutable distributions solve this by treating the operating system as a read-only image. When you update the system, you aren&#8217;t just changing individual files; you are switching to a completely new, pre-verified version of the OS. This makes the system &#8220;atomic&#8221;, it either works perfectly, or it rolls back to the previous version.<\/p>\n<h2>The Problem: Seeing the &#8220;Read-Only&#8221; Barrier<\/h2>\n<p>While immutability is great for stability, it\u2019s a nightmare for &#8220;on-the-fly&#8221; troubleshooting. On a standard system, if I need to see why a network port is blocked, I might quickly install <code>nmap<\/code> or <code>tcpdump<\/code>. On an immutable system, I\u2019m stuck.<\/p>\n<p>You can see this in action by trying to manually add a file to your system binaries:<\/p>\n<pre><code>sudo touch \/usr\/bin\/test_file<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-52-1.png\" class=\"kg-image\" alt=\"read only file system\" loading=\"lazy\" width=\"820\" height=\"151\" \/><\/figure>\n<p>Instead of creating a file, you\u2019ll get a rejection message: <code>touch: cannot touch '\/usr\/bin\/test_file': Read-only file system<\/code>. This proves that even with <code>sudo<\/code>, the core of your OS is locked. <\/p>\n<p>To add a tool &#8220;the official way&#8221; (layering), you\u2019d have to run a command like <code>rpm-ostree install<\/code> and then restart your computer. For a quick task, that&#8217;s a massive interruption. And this &#8220;rpm-ostree&#8221; is more of a Fedora Silverblue thing, it won&#8217;t work on non-Fedora atomic distros.<\/p>\n<h2>How System Extensions Actually Work<\/h2>\n<p>I&#8217;d like to think of <code>systemd-sysext<\/code> as a digital &#8220;overlay.&#8221; Instead of fighting the read-only filesystem, we are going to build a small directory structure that contains our tools and tell the system to virtually &#8220;merge&#8221; it on top of the existing <code>\/usr<\/code>.<\/p>\n<p>This uses a kernel feature called <strong>OverlayFS<\/strong>. It takes your base (read-only) system as the &#8220;Lower&#8221; layer and your extension as the &#8220;Upper&#8221; layer. The result is a &#8220;Merged&#8221; view that the user interacts with. To your applications, it looks like the files were there all along.<\/p>\n<h3>Step 1: Building Your First System Extension<\/h3>\n<p>You don&#8217;t need complex build systems to create a system extension. At its simplest, a <code>sysext<\/code> is just a directory structure that mirrors the Linux root. Let&#8217;s build a workspace for a custom tool.<\/p>\n<p>First, mirror the Linux filesystem hierarchy:<\/p>\n<pre><code>mkdir -p my-tool-ext\/usr\/bin\nmkdir -p my-tool-ext\/usr\/lib\/extension-release.d\/\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-53-1.png\" class=\"kg-image\" alt=\"Creating the base hierarchy.\" loading=\"lazy\" width=\"847\" height=\"164\" \/><\/figure>\n<p>Next, let&#8217;s create a simple test tool. In a real-world scenario, you could drop a compiled binary like <code>ncdu<\/code> or <code>htop<\/code> here, but for this guide, a script works perfectly:<\/p>\n<pre><code>echo -e '#!\/bin\/shnecho \"Sysext is active on Fedora!\"' &gt; my-tool-ext\/usr\/bin\/foss-tool\nchmod +x my-tool-ext\/usr\/bin\/foss-tool\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-54-1.png\" class=\"kg-image\" alt=\"Creating a custom shell script and setting execution permissions using chmod.\" loading=\"lazy\" width=\"973\" height=\"156\" \/><\/figure>\n<h3>Step 2: The Metadata &#8220;Passport&#8221;<\/h3>\n<p>This is the most critical step and where you can get stuck. <code>systemd-sysext<\/code> acts as a gatekeeper. It will not merge an extension unless it knows exactly which OS version the extension is built for. To find out what your system expects, run:<\/p>\n<p><code>cat \/etc\/os-release | grep -E '^ID=|^VERSION_ID='<\/code><\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-55-1.png\" class=\"kg-image\" alt=\"Checking Fedora OS release version.\" loading=\"lazy\" width=\"966\" height=\"175\" \/><\/figure>\n<p>On my setup, this returns <code>ID=fedora<\/code> and <code>VERSION_ID=43<\/code>. <strong> <\/strong>If you are following along, make sure to replace these values with whatever your specific system reports. If you are on Silverblue 39, use those numbers. <\/p>\n<p>Now, create the mandatory release file:<\/p>\n<pre><code>echo \"ID=fedora\" &gt; my-tool-ext\/usr\/lib\/extension-release.d\/extension-release.my-tool-ext\necho \"VERSION_ID=43\" &gt;&gt; my-tool-ext\/usr\/lib\/extension-release.d\/extension-release.my-tool-ext\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-56-1.png\" class=\"kg-image\" alt=\"Creating extension release metadata file\" loading=\"lazy\" width=\"976\" height=\"143\" \/><\/figure>\n<p>Before we merge anything into the live system, it\u2019s worth double-checking our work. A <code>systemd-sysext<\/code> image is essentially a mirror of your root directory, so the file hierarchy must be exact. You can verify your layout by running: <\/p>\n<pre><code>ls -R my-tool-ext\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-60.png\" class=\"kg-image\" alt=\"Verifying sysext directory structure.\" loading=\"lazy\" width=\"925\" height=\"445\" \/><\/figure>\n<p>You should see your binary sitting in <code>usr\/bin<\/code> and your metadata &#8216;passport&#8217; tucked away in <code>usr\/lib\/extension-release.d\/<\/code>. If these aren&#8217;t in the right place, the system simply won&#8217;t know how to &#8216;map&#8217; them during the merge.<\/p>\n<h3>Step 3: The &#8220;Merge&#8221; Moment<\/h3>\n<p>Now that our extension has its &#8220;passport&#8221; ready, we move it to the system&#8217;s extension path and trigger the merge. This is the moment where the read-only barrier is bypassed:<\/p>\n<pre><code>sudo cp -r my-tool-ext \/var\/lib\/extensions\/\nsudo systemd-sysext merge\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-59-1.png\" class=\"kg-image\" alt=\"Executing the systemd-sysext merge command on Fedora\" loading=\"lazy\" width=\"977\" height=\"169\" \/><\/figure>\n<p>Next, confirm the binary location. The system should see it as a standard system tool:<\/p>\n<pre><code>ls -l \/usr\/bin\/foss-tool\nfoss-tool\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-57-1.png\" class=\"kg-image\" alt=\"Verifying custom binary in usr-bin\" loading=\"lazy\" width=\"926\" height=\"153\" \/><\/figure>\n<p>You can verify the status and run your new tool:<\/p>\n<pre><code>systemd-sysext status\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-58-1.png\" class=\"kg-image\" alt=\"Checking the active systemd-sysext extensions.\" loading=\"lazy\" width=\"931\" height=\"177\" \/><\/figure>\n<p>Your system is still technically read-only, but you\u2019ve successfully injected new functionality into it without a single reboot.<\/p>\n<h2>Troubleshooting: When the Merge Fails<\/h2>\n<p>One of the most common frustrations is seeing the error: <code>No suitable extensions found (1 ignored due to incompatible image)<\/code>. This isn&#8217;t a bug; it&#8217;s a safety feature.<\/p>\n<p>If your <code>extension-release<\/code> file says you are on Fedora 42 but you actually just upgraded to Fedora 43, <code>systemd<\/code> will block the merge. <\/p>\n<p>It does this because libraries often change between versions, and merging an incompatible binary could cause system instability. If you hit this error, simply update your metadata to match your current <code>os-release<\/code> and re-run the merge.<\/p>\n<h2>Reverting Without a Trace<\/h2>\n<p>The most powerful feature of <code>systemd-sysext<\/code> isn&#8217;t just how easily it adds tools, it\u2019s how cleanly it removes them. Traditional package management often leaves behind config files or libraries that clutter your system over time. <\/p>\n<p>With <code>sysext<\/code>, unmerging is a clean break:<\/p>\n<pre><code>sudo systemd-sysext unmerge\n<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/itsfoss.com\/content\/images\/2026\/03\/image-61-1.png\" class=\"kg-image\" alt=\"Running systemd-sysext-unmerge for system cleanup.\" loading=\"lazy\" width=\"977\" height=\"161\" \/><\/figure>\n<p>If you try to run your tool now, the shell will return a <code>No such file or directory<\/code> error. The overlay has been lifted, and your <code>\/usr<\/code> directory is exactly as it was when you first installed the OS.<\/p>\n<h2>Why This Beats the Container Approach<\/h2>\n<p>A common question is: &#8220;Why not just use <a href=\"https:\/\/itsfoss.com\/distrobox\/\" rel=\"noreferrer\">Distrobox<\/a>?&#8221; Containers are amazing for general applications, but they run in an isolated namespace. If you are trying to debug a kernel issue or analyze hardware peripherals, that isolation can get in the way.<\/p>\n<p><code>systemd-sysext<\/code> puts the tool <strong>directly on the host<\/strong>. It has the same permissions and visibility as a tool shipped with the OS itself. If you need a tool to &#8220;be&#8221; the system rather than just &#8220;run on&#8221; the system, <code>sysext<\/code> is the surgical choice.<\/p>\n<h2>Conclusion<\/h2>\n<p>The move toward immutable Linux shouldn&#8217;t feel like a move toward a &#8220;locked-down&#8221; experience. Tools like <code>systemd-sysext<\/code> prove that we can have our cake and eat it too. We get the security of a read-only core and the flexibility to inject any tool we need instantly.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/feed.itsfoss.com\/link\/24361\/17309280.gif\" height=\"1\" width=\"1\" \/><\/p>","protected":false},"excerpt":{"rendered":"<p>If you\u2019ve recently dipped your toes into the world of immutable Linux distributions like Fedora Silverblue, openSUSE MicroOS, or even the Steam Deck, you&#8217;ll encounter this issue eventually. You try&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-5327","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-rss"],"_links":{"self":[{"href":"https:\/\/catbradley.io\/index.php?rest_route=\/wp\/v2\/posts\/5327","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/catbradley.io\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/catbradley.io\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/catbradley.io\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/catbradley.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5327"}],"version-history":[{"count":0,"href":"https:\/\/catbradley.io\/index.php?rest_route=\/wp\/v2\/posts\/5327\/revisions"}],"wp:attachment":[{"href":"https:\/\/catbradley.io\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5327"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/catbradley.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5327"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/catbradley.io\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5327"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}