<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Arthur Perrot — Full journal</title><link>https://perrot.pt/posts/</link><description>Every article — field notes, blue team, homelab and automation.</description><generator>Hugo</generator><language>en</language><copyright>© 2026 Arthur Perrot</copyright><lastBuildDate>Fri, 23 Oct 2026 12:00:00 +0000</lastBuildDate><atom:link href="https://perrot.pt/categories/open-source/index.xml" rel="self" type="application/rss+xml"/><item><title>Spotify no longer starts after a hostname change (Linux)</title><link>https://perrot.pt/posts/spotify-wont-start-after-a-hostname-change-linux/</link><pubDate>Fri, 23 Oct 2026 12:00:00 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><category>Quick tips</category><guid isPermaLink="true">https://perrot.pt/posts/spotify-wont-start-after-a-hostname-change-linux/</guid><description>Spotify won't open and exits with code 1 after a hostname change: an orphaned Singleton* lock, and how to remove it.</description><content:encoded><![CDATA[<h2 id="the-symptom">The symptom</h2>
<p>Spotify (official <code>.deb</code> package, <code>spotify-client</code> 1.2.95, Debian 13) no longer opens. No window, no message. From the launcher, nothing happens. From a terminal:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">console</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> spotify
</span></span><span class="line"><span class="cl"><span class="gp">$</span> <span class="nb">echo</span> <span class="nv">$?</span>
</span></span><span class="line"><span class="cl"><span class="go">1
</span></span></span></code></pre></div></div>
<p>With <code>--enable-logging=stderr</code>, a single line is printed:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">INFO:cef/libcef/common/crash_reporting.cc:209] Crash reporting enabled for process: browser</span></span></code></pre></div></div>
<p>Then the process exits with code 1.</p>
<h2 id="false-leads">False leads</h2>
<ul>
<li><strong>The GPU</strong>: <code>--disable-gpu</code>, <code>--in-process-gpu</code>, <code>--ozone-platform=x11|wayland</code>, <code>--use-gl=egl</code> change nothing, the exit code stays 1.</li>
<li><strong>The package</strong>: no recent Spotify update, the binary is intact.</li>
<li><strong>The config</strong>: <code>~/.config/spotify/prefs</code> is readable and consistent.</li>
</ul>
<p>A test with a clean profile (<code>XDG_CONFIG_HOME</code> and <code>XDG_CACHE_HOME</code> pointed to a temporary folder) did start. So the problem came from the existing profile, not from the installation.</p>
<h2 id="the-cause">The cause</h2>
<p>Spotify is a CEF (Chromium Embedded Framework) application. Like Chromium, it only allows one instance per profile, with three symbolic links in <code>~/.cache/spotify</code>:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SingletonLock   -&gt; huntervcx-imac151-10745
</span></span><span class="line"><span class="cl">SingletonCookie -&gt; 11476635766705238864
</span></span><span class="line"><span class="cl">SingletonSocket -&gt; /tmp/org.chromium.Chromium.sCe4Pv/SingletonSocket</span></span></code></pre></div></div>
<p>The content of <code>SingletonLock</code> is <code>&lt;hostname&gt;-&lt;pid&gt;</code>. At startup, Chromium reads this link:</p>
<ul>
<li>if the hostname matches the current machine, it checks whether the PID is still running and takes over the lock if it is dead;</li>
<li><strong>if the hostname differs, it assumes the profile is used by another machine (profile on a network share) and leaves everything alone.</strong></li>
</ul>
<p>Here, the lock had been left the day before by a session that hadn&rsquo;t closed the app properly, under the old hostname <code>huntervcx-imac151</code>. In the meantime, the machine had been renamed <code>iMac-Arthur</code>. Spotify therefore saw a lock from &ldquo;another machine&rdquo; and quit silently.</p>
<h2 id="the-fix">The fix</h2>
<p>Make sure no Spotify is running, then delete the three links:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pgrep -af spotify
</span></span><span class="line"><span class="cl">rm ~/.cache/spotify/SingletonLock <span class="se">\
</span></span></span><span class="line"><span class="cl">   ~/.cache/spotify/SingletonCookie <span class="se">\
</span></span></span><span class="line"><span class="cl">   ~/.cache/spotify/SingletonSocket</span></span></code></pre></div></div>
<p>These files contain only the lock: no account, no track cache, no preferences. Spotify starts normally again.</p>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li>Changing the hostname while a Chromium/Electron/CEF app has crashed or is still open leaves an orphaned lock that the app can no longer recover from.</li>
<li>The same symptom can occur with Chrome, Edge, VS Code, Discord, etc.: run <code>ls -la</code> on the profile folder and look at the <code>Singleton*</code> files.</li>
<li>An exit code of 1 with no message is often a lock or a profile issue, not a GPU problem.</li>
<li>If you change the hostname, close your applications first, or clean up the <code>Singleton*</code> files afterwards.</li>
</ul>
]]></content:encoded></item><item><title>Minecraft 26.3 (Prism): transparent item icons</title><link>https://perrot.pt/posts/minecraft-26-3-prism-transparent-item-icons/</link><pubDate>Fri, 23 Oct 2026 12:00:00 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Quick tips</category><guid isPermaLink="true">https://perrot.pt/posts/minecraft-26-3-prism-transparent-item-icons/</guid><description>Transparent item icons in Minecraft 26.3 on Prism Launcher: the icon atlas lead, the GPU red herring and the real cause, SDL falling back to XWayland.</description><content:encoded><![CDATA[<p>Minecraft 26.3, launched from Prism Launcher on Linux: when I open the creative inventory, some of the item icons are transparent. The items are there (you can pick them up), you just can&rsquo;t see them. Here is how I ended up finding the cause, and why the right lead wasn&rsquo;t the first one.</p>
<h2 id="the-symptom">The symptom</h2>
<p>Instance: <code>~/.local/share/PrismLauncher/instances/26.3/minecraft</code>. 5120x2880 fullscreen display, <code>guiScale:3</code>. In <code>logs/latest.log</code>, when opening the creative inventory:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Too many items (64) in UI, some will be skipped! (Reached maximum texture size 512x512)</span></span></code></pre></div></div>
<p>There is a quick in-game fix: <code>F3+T</code> (reload packs) brings the icons back, until the next time.</p>
<h2 id="first-lead-the-icon-atlas">First lead: the icon atlas</h2>
<p>The UI item icons are drawn into an atlas capped at 512x512 px. If not everything fits, the extra items are skipped: transparent icon. It doesn&rsquo;t depend on the resource pack, and with a 5120x2880 display at <code>guiScale:3</code> the icons are large, so the atlas fills up quickly.</p>
<p>My back-of-the-envelope hypothesis (not confirmed): an icon is 16 px x <code>guiScale</code>, so a 512x512 atlas holds 64 icons at <code>guiScale:4</code> (64 px), about 100 at 3 and 256 at 2. It matches the logs: the message appeared on the 21st (14:07, 66 items) and the 22nd (22:30:53, 64 items).</p>
<p>Test: <code>guiScale:2</code> helps, but the interface becomes too small. A workaround, not a fix.</p>
<h2 id="dead-end-driver-and-kernel">Dead end: driver and kernel</h2>
<ul>
<li>Kernel <code>7.2.11-imac5kstitch</code>, <code>amdgpu</code> OK.</li>
<li>OpenGL works on the system: <code>eglinfo</code> on Wayland reports radeonsi pitcairn, GL 4.6, and <code>glxinfo</code> on XWayland is accelerated.</li>
</ul>
<p>But in Minecraft, before the window is even created:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">GlBackend.loadLibrary ... Could not retrieve OpenGL functions</span></span></code></pre></div></div>
<p>The game then falls back to Vulkan (RADV PITCAIRN, Mesa 25.0.7), and that works. The <code>Requested swapchain extent ... not within allowed extent</code> messages are just fullscreen resizing under XWayland, harmless.</p>
<h2 id="the-real-problem-sdl-falls-back-to-xwayland">The real problem: SDL falls back to XWayland</h2>
<p>I had tried forcing OpenGL with <code>preferredGraphicsBackend:&quot;default&quot;</code> in <code>options.txt</code> (game closed, otherwise it rewrites the file). OpenGL still failed with the same error and the game went back to Vulkan: that test had never really happened.</p>
<p>The cause: SDL was falling back to XWayland, because my compositor doesn&rsquo;t support <code>fifo-v1</code>. The fix is an environment variable on the Prism instance (instance settings, environment variables, with <code>OverrideEnv=true</code>):</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SDL_VIDEO_DRIVER=wayland</span></span></code></pre></div></div>
<h2 id="result">Result</h2>
<p>Test on 23 October at 10:25: OpenGL 4.6 radeonsi pitcairn, SDL window on <code>wayland</code>, <code>guiScale:3</code>, singleplayer world. No more icon problem and no more <code>Too many items</code>.</p>
<p>Still to check: on the server, with more items (multiplayer creative), once it is back up. If the warning comes back, I&rsquo;ll have to see whether it comes from the server or from the backend. In the meantime, my take: the real culprit was the XWayland fallback, not the atlas size.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li>If OpenGL fails in Minecraft while <code>eglinfo</code> and <code>glxinfo</code> work, check which backend SDL is really using.</li>
<li>On a Wayland compositor without <code>fifo-v1</code>, SDL can fall back to XWayland. <code>SDL_VIDEO_DRIVER=wayland</code> in the Prism instance environment fixes it.</li>
<li>A test is only worth something if you&rsquo;ve checked it tests what you think it does (here, the &ldquo;OpenGL test&rdquo; was actually running on Vulkan).</li>
</ul>
]]></content:encoded></item><item><title>Setting Up a Cloudflare Dynamic DNS on Debian with systemd</title><link>https://perrot.pt/posts/cloudflare-dynamic-dns-debian-systemd/</link><pubDate>Tue, 29 Sep 2026 12:00:00 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><guid isPermaLink="true">https://perrot.pt/posts/cloudflare-dynamic-dns-debian-systemd/</guid><description>A homemade DDNS on Debian: a Bash script, the Cloudflare API and a systemd timer to keep an A record in sync with a dynamic public IP.</description><content:encoded><![CDATA[<p>When you use a laptop or a server behind an Internet connection with a dynamic public IPv4 address, the address can change at any time.</p>
<p>The goal is simple:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">arthur-perrot.example.com
</span></span><span class="line"><span class="cl">        ↓
</span></span><span class="line"><span class="cl">Current public IPv4 address</span></span></code></pre></div></div>
<p>The machine regularly checks its public IP and, when it changes, automatically updates the matching DNS record at Cloudflare.</p>
<p>In this guide, we will set up this DDNS with a simple Bash script, the Cloudflare API and a systemd timer on Debian.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>You need:</p>
<ul>
<li>a domain whose DNS is managed by Cloudflare;</li>
<li>Debian or another distribution using systemd;</li>
<li><code>curl</code>;</li>
<li><code>jq</code>;</li>
<li>a Cloudflare API token.</li>
</ul>
<p>Installing the dependencies:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install curl jq</span></span></code></pre></div></div>
<p>In the examples, we will use:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">DNS zone : example.com
</span></span><span class="line"><span class="cl">Record   : arthur-perrot.example.com</span></span></code></pre></div></div>
<h2 id="1-create-the-cloudflare-api-token">1. Create the Cloudflare API token</h2>
<p>From Cloudflare, create an <strong>API Token</strong> with only the permissions required on the relevant zone.</p>
<p>Recommended permissions:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Zone → DNS → Edit
</span></span><span class="line"><span class="cl">Zone → Zone → Read</span></span></code></pre></div></div>
<p>It is better to limit the token to a single DNS zone rather than giving it access to the whole account.</p>
<p>You can check that the token works:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">CF_API_TOKEN</span><span class="o">=</span><span class="s1">&#39;cfat_xxxxxxxxxxxxxxxxx&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">curl -s <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$CF_API_TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://api.cloudflare.com/client/v4/zones?name=example.com&#34;</span> <span class="p">|</span> jq</span></span></code></pre></div></div>
<p>Cloudflare should return, among other things:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">json</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;success&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The response also contains the zone identifier:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">json</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;id&#34;</span><span class="err">:</span> <span class="s2">&#34;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#34;</span></span></span></code></pre></div></div>
<p>Keep this <strong>Zone ID</strong>, we will use it directly in the script.</p>
<h2 id="2-create-the-dns-record">2. Create the DNS record</h2>
<p>In Cloudflare, create an <code>A</code> record, for example:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Type : A
</span></span><span class="line"><span class="cl">Name : arthur-perrot
</span></span><span class="line"><span class="cl">IPv4 : 1.2.3.4
</span></span><span class="line"><span class="cl">TTL  : Auto</span></span></code></pre></div></div>
<p>The initial address does not matter: the script will replace it on its first run.</p>
<p>You can check the resolution with:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short arthur-perrot.example.com @1.1.1.1</span></span></code></pre></div></div>
<p>For example:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1.2.3.4</span></span></code></pre></div></div>
<h2 id="3-create-the-ddns-script">3. Create the DDNS script</h2>
<p>Let&rsquo;s create our script:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /usr/local/bin/cloudflare-ddns</span></span></code></pre></div></div>
<p>Content:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -e
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">TOKEN</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$CF_API_TOKEN</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">ZONE_ID</span><span class="o">=</span><span class="s2">&#34;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">RECORD</span><span class="o">=</span><span class="s2">&#34;arthur-perrot.example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fetch the public IPv4 address</span>
</span></span><span class="line"><span class="cl"><span class="nv">IP</span><span class="o">=</span><span class="k">$(</span>curl -fsS -4 https://api.ipify.org<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Look up the DNS record identifier</span>
</span></span><span class="line"><span class="cl"><span class="nv">RECORD_ID</span><span class="o">=</span><span class="k">$(</span>curl -fsS <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://api.cloudflare.com/client/v4/zones/</span><span class="nv">$ZONE_ID</span><span class="s2">/dns_records?type=A&amp;name=</span><span class="nv">$RECORD</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq -r <span class="s1">&#39;.result[0].id&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$RECORD_ID</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;null&#34;</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="nv">$RECORD_ID</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Record </span><span class="nv">$RECORD</span><span class="s2"> introuvable&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fetch the address currently published</span>
</span></span><span class="line"><span class="cl"><span class="nv">CURRENT</span><span class="o">=</span><span class="k">$(</span>curl -fsS <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://api.cloudflare.com/client/v4/zones/</span><span class="nv">$ZONE_ID</span><span class="s2">/dns_records/</span><span class="nv">$RECORD_ID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq -r <span class="s1">&#39;.result.content&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Nothing to do if the address has not changed</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$CURRENT</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$IP</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;IP inchangée : </span><span class="nv">$IP</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Update the Cloudflare record</span>
</span></span><span class="line"><span class="cl">curl -fsS -X PATCH <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://api.cloudflare.com/client/v4/zones/</span><span class="nv">$ZONE_ID</span><span class="s2">/dns_records/</span><span class="nv">$RECORD_ID</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --data <span class="s2">&#34;{\&#34;content\&#34;:\&#34;</span><span class="nv">$IP</span><span class="s2">\&#34;}&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq -e <span class="s1">&#39;.success&#39;</span> &gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$RECORD</span><span class="s2"> : </span><span class="nv">$CURRENT</span><span class="s2"> -&gt; </span><span class="nv">$IP</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
<p>Make the script executable:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chmod <span class="m">700</span> /usr/local/bin/cloudflare-ddns</span></span></code></pre></div></div>
<p>The logic is intentionally simple:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Fetch the public IP
</span></span><span class="line"><span class="cl">              ↓
</span></span><span class="line"><span class="cl">Read the Cloudflare record
</span></span><span class="line"><span class="cl">              ↓
</span></span><span class="line"><span class="cl">  Same IP?
</span></span><span class="line"><span class="cl">        ↙           ↘
</span></span><span class="line"><span class="cl">      Yes           No
</span></span><span class="line"><span class="cl">       ↓             ↓
</span></span><span class="line"><span class="cl">   Do nothing    Update
</span></span><span class="line"><span class="cl">                 Cloudflare</span></span></code></pre></div></div>
<h2 id="4-store-the-token-separately">4. Store the token separately</h2>
<p>It is better not to put the Cloudflare token directly in the script.</p>
<p>Create:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/cloudflare-ddns.env</span></span></code></pre></div></div>
<p>With:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">CF_API_TOKEN</span><span class="o">=</span><span class="s">cfat_xxxxxxxxxxxxxxxxx</span></span></span></code></pre></div></div>
<p>Then restrict the permissions:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chown root:root /etc/cloudflare-ddns.env
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">600</span> /etc/cloudflare-ddns.env</span></span></code></pre></div></div>
<p>This way, only <code>root</code> can read the token.</p>
<h2 id="5-create-the-systemd-service">5. Create the systemd service</h2>
<p>Create:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/systemd/system/cloudflare-ddns.service</span></span></code></pre></div></div>
<p>With:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Cloudflare DDNS updater</span>
</span></span><span class="line"><span class="cl"><span class="na">Wants</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">EnvironmentFile</span><span class="o">=</span><span class="s">/etc/cloudflare-ddns.env</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/cloudflare-ddns</span></span></span></code></pre></div></div>
<p>Then reload systemd:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl daemon-reload</span></span></code></pre></div></div>
<p>Let&rsquo;s test the service manually:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl start cloudflare-ddns</span></span></code></pre></div></div>
<p>Then:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl status cloudflare-ddns</span></span></code></pre></div></div>
<p>A result like this one indicates that everything works:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Starting cloudflare-ddns.service - Cloudflare DDNS updater...
</span></span><span class="line"><span class="cl">arthur-perrot.example.com : 1.2.3.4 -&gt; 203.0.113.42
</span></span><span class="line"><span class="cl">Deactivated successfully.
</span></span><span class="line"><span class="cl">Finished cloudflare-ddns.service - Cloudflare DDNS updater.</span></span></code></pre></div></div>
<p>On subsequent runs, if the IP has not changed:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">IP inchangée : 203.0.113.42</span></span></code></pre></div></div>
<p>It is normal for the service to then show as:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">inactive (dead)</span></span></code></pre></div></div>
<p>This is a <code>oneshot</code> service: it does its job and then exits.</p>
<h2 id="6-run-the-ddns-automatically-every-5-minutes">6. Run the DDNS automatically every 5 minutes</h2>
<p>We can now use a systemd timer.</p>
<p>Create:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/systemd/system/cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>With:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Update Cloudflare DDNS every 5 minutes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnBootSec</span><span class="o">=</span><span class="s">30s</span>
</span></span><span class="line"><span class="cl"><span class="na">OnUnitActiveSec</span><span class="o">=</span><span class="s">5min</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span></span></span></code></pre></div></div>
<p>Enable the timer:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>Check its status:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl status cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>You should get:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Active: active (waiting)</span></span></code></pre></div></div>
<p>To see the next run:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl list-timers cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>For example:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NEXT                        LEFT     UNIT
</span></span><span class="line"><span class="cl">Tue 17:43:20 EDT            4min     cloudflare-ddns.timer</span></span></code></pre></div></div>
<h2 id="7-check-the-logs">7. Check the logs</h2>
<p>Script runs are recorded in the systemd journal.</p>
<p>Show the latest runs:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -u cloudflare-ddns.service -n <span class="m">20</span></span></span></code></pre></div></div>
<p>Follow the next runs live:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -fu cloudflare-ddns.service</span></span></code></pre></div></div>
<p>To show only the events since the last boot:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">journalctl -b -u cloudflare-ddns.service</span></span></code></pre></div></div>
<h2 id="8-check-after-a-reboot">8. Check after a reboot</h2>
<p>The timer was enabled with:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>so it will start automatically with Debian.</p>
<p>You can check:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl is-enabled cloudflare-ddns.timer
</span></span><span class="line"><span class="cl">systemctl is-active cloudflare-ddns.timer</span></span></code></pre></div></div>
<p>Expected result:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">enabled
</span></span><span class="line"><span class="cl">active</span></span></code></pre></div></div>
<p>About 30 seconds after boot, a first check will run. After that, the script will run every five minutes.</p>
<h2 id="9-check-the-result">9. Check the result</h2>
<p>Show the public IPv4 address:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -4 -s https://api.ipify.org
</span></span><span class="line"><span class="cl">echo</span></span></code></pre></div></div>
<p>Then check the DNS:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short arthur-perrot.example.com @1.1.1.1</span></span></code></pre></div></div>
<p>Both values must match:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">203.0.113.42
</span></span><span class="line"><span class="cl">203.0.113.42</span></span></code></pre></div></div>
<p>The final flow is therefore:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">                    ┌─────────────────┐
</span></span><span class="line"><span class="cl">                    │   Debian PC     │
</span></span><span class="line"><span class="cl">                    └────────┬────────┘
</span></span><span class="line"><span class="cl">                             │
</span></span><span class="line"><span class="cl">                      every 5 min
</span></span><span class="line"><span class="cl">                             │
</span></span><span class="line"><span class="cl">                             ▼
</span></span><span class="line"><span class="cl">                    Current public IP
</span></span><span class="line"><span class="cl">                             │
</span></span><span class="line"><span class="cl">                             ▼
</span></span><span class="line"><span class="cl">                    Compare with Cloudflare
</span></span><span class="line"><span class="cl">                             │
</span></span><span class="line"><span class="cl">                    ┌────────┴────────┐
</span></span><span class="line"><span class="cl">                    │                 │
</span></span><span class="line"><span class="cl">                  same            different
</span></span><span class="line"><span class="cl">                    │                 │
</span></span><span class="line"><span class="cl">                    ▼                 ▼
</span></span><span class="line"><span class="cl">                 nothing       Cloudflare API
</span></span><span class="line"><span class="cl">                                      │
</span></span><span class="line"><span class="cl">                                      ▼
</span></span><span class="line"><span class="cl">                         arthur-perrot.example.com
</span></span><span class="line"><span class="cl">                                      │
</span></span><span class="line"><span class="cl">                                      ▼
</span></span><span class="line"><span class="cl">                             new public IPv4</span></span></code></pre></div></div>
<p>The advantage of this solution is that it does not require any extra DDNS daemon: only <code>curl</code>, <code>jq</code>, a few lines of Bash and the native systemd mechanisms.</p>
<p>It is particularly suitable for a laptop that often changes networks: home, phone hotspot, public Wi-Fi, etc.</p>
<p>Keep in mind, however, that DDNS does not bypass NAT or network restrictions. The DNS name will point to the public IP address used by the laptop, but this does not guarantee that incoming connections to it are possible, especially behind CGNAT or on a network that blocks incoming connections.</p>
]]></content:encoded></item><item><title>Claude Code + tmux: Stop Losing Your Terminal Sessions Every Time Your Mac Sleeps</title><link>https://perrot.pt/posts/claude-code-tmux-stop-losing-your-terminal-sessions-every-time-your-mac-sleeps/</link><pubDate>Tue, 02 Jun 2026 00:03:12 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><category>Quick tips</category><category>Dev</category><guid isPermaLink="true">https://perrot.pt/posts/claude-code-tmux-stop-losing-your-terminal-sessions-every-time-your-mac-sleeps/</guid><description>Run Claude Code inside a persistent tmux session with a cc alias, so closing the terminal or the laptop no longer kills the agent or its context.</description><content:encoded><![CDATA[<p>You launch a Claude Code session on your Mac.</p>
<p>Then:</p>
<ul>
<li>you close the lid,</li>
<li>the terminal crashes,</li>
<li>your SSH session disconnects,</li>
<li>or VSCode restarts.</li>
</ul>
<p>And the entire session disappears.</p>
<p>Context gone. History gone. Work gone.</p>
<p>The issue is not Claude Code itself.</p>
<p>The issue is that the agent is running directly inside your active terminal session.</p>
<p>When the terminal dies, the process dies with it.</p>
<h2 id="why-tmux-completely-changes-the-claude-code-experience">Why tmux Completely Changes the Claude Code Experience</h2>
<p><code>tmux</code> lets you detach terminal sessions from your current terminal window.</p>
<p>That means:</p>
<ul>
<li>your Claude session keeps running even if the terminal closes,</li>
<li>you can reconnect later exactly where you left off,</li>
<li>Claude keeps its loaded context,</li>
<li>and long-running tasks continue in the background.</li>
</ul>
<p>This is especially useful:</p>
<ul>
<li>over SSH on VPS instances,</li>
<li>for long-running sessions,</li>
<li>in multi-project workflows,</li>
<li>with multiple Claude agents,</li>
<li>on remote servers,</li>
<li>or simply to avoid keeping a MacBook awake 24/7.</li>
</ul>
<h2 id="the-setup-a-cc-alias">The Setup: a <code>cc</code> Alias</h2>
<p>The goal:</p>
<ul>
<li>automatically create a tmux session,</li>
<li>launch <code>claude remote-control</code>,</li>
<li>instantly attach your terminal,</li>
<li>generate a unique session name per project.</li>
</ul>
<h2 id="quick-installation">Quick Installation</h2>
<h3 id="bash">Bash</h3>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;alias cc=&#39;SESSION=claude-\$(basename \&#34;\$PWD\&#34;)-\$(date +%s); tmux new-session -d -s \$SESSION \&#34;cd \\\&#34;\$PWD\\\&#34; &amp;&amp; claude remote-control\&#34; &amp;&amp; tmux attach -t \$SESSION&#39;&#34;</span> &gt;&gt; ~/.bashrc <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/.bashrc</span></span></code></pre></div></div>
<h3 id="zsh">Zsh</h3>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;alias cc=&#39;SESSION=claude-\$(basename \&#34;\$PWD\&#34;)-\$(date +%s); tmux new-session -d -s \$SESSION \&#34;cd \\\&#34;\$PWD\\\&#34; &amp;&amp; claude remote-control\&#34; &amp;&amp; tmux attach -t \$SESSION&#39;&#34;</span> &gt;&gt; ~/.zshrc <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/.zshrc</span></span></code></pre></div></div>
<h2 id="usage">Usage</h2>
<p>From any project directory:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cc</span></span></code></pre></div></div>
<p>The alias will:</p>
<ol>
<li>create a dedicated tmux session,</li>
<li>launch Claude Code inside the current project,</li>
<li>automatically attach your terminal to the session.</li>
</ol>
<h2 id="reconnecting-to-a-claude-session">Reconnecting to a Claude Session</h2>
<p>Even after closing your terminal, the session remains active.</p>
<p>To list active sessions:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tmux ls</span></span></code></pre></div></div>
<p>To reconnect to a session:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tmux attach -t session-name</span></span></code></pre></div></div>
<h2 id="why-this-matters-for-engineering-teams">Why This Matters for Engineering Teams</h2>
<p>In real-world infrastructure and development environments, this approach helps:</p>
<ul>
<li>maintain persistent AI agents,</li>
<li>reduce context loss,</li>
<li>improve SSH workflows,</li>
<li>streamline DevOps operations,</li>
<li>run multiple Claude environments simultaneously,</li>
<li>and avoid interruptions caused by user workstations.</li>
</ul>
<p>It is especially useful for:</p>
<ul>
<li>Kubernetes,</li>
<li>CI/CD pipelines,</li>
<li>Linux administration,</li>
<li>backend development,</li>
<li>automation,</li>
<li>cybersecurity,</li>
<li>and long-running operational tasks.</li>
</ul>
<h2 id="bonus-clean-multi-project-isolation">Bonus: Clean Multi-Project Isolation</h2>
<p>Because the session name automatically includes the current directory, every project gets its own isolated Claude environment.</p>
<p>Example:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">text</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">claude-api-1748812941
</span></span><span class="line"><span class="cl">claude-frontend-1748812958
</span></span><span class="line"><span class="cl">claude-infra-prod-1748812972</span></span></code></pre></div></div>
<p>You can work across multiple projects simultaneously without conflicts.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Most developers use Claude Code like an advanced terminal assistant.</p>
<p>But once paired with <code>tmux</code>, it becomes a persistent AI workspace.</p>
<p>The difference is immediate:</p>
<ul>
<li>fewer interruptions,</li>
<li>fewer lost sessions,</li>
<li>less context rebuilding,</li>
<li>and far more stable workflows.</li>
</ul>
<p>For advanced engineering workflows, it quickly becomes difficult to work without it.</p>
]]></content:encoded></item><item><title>The Client Doesn’t Pay for Your Perfect Architecture</title><link>https://perrot.pt/posts/the-client-doesnt-pay-for-your-perfect-architecture/</link><pubDate>Mon, 09 Mar 2026 10:35:00 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Dev</category><guid isPermaLink="true">https://perrot.pt/posts/the-client-doesnt-pay-for-your-perfect-architecture/</guid><description>Perfect architecture or code that ships? A candid take on technical debt, the Pareto principle and what the client actually buys.</description><content:encoded><![CDATA[<p>There are two major schools of thought among developers.</p>
<p>Those who talk about hexagonal architecture, clean code, elegant patterns, strict separation of concerns.<br>
And those who ship.</p>
<p>I&rsquo;m exaggerating on purpose. But the debate does exist.</p>
<p>And I&rsquo;ll say it loud and clear: <strong>I&rsquo;m a bad developer</strong>.<br>
Five years at Epitech taught me to code, but I don&rsquo;t enjoy it. So I&rsquo;m not the best.<br>
Still, I outperform most of your devs, because this weakness is my biggest asset for my clients.</p>
<hr>
<h2 id="the-myth-of-perfect-code">The Myth of Perfect Code</h2>
<p>We&rsquo;ve all come across this profile:</p>
<ul>
<li>14 layers of abstraction,</li>
<li>12 interfaces for a single implementation,</li>
<li>a “future-proof” architecture,</li>
<li>zero visible features for the client.</li>
</ul>
<p>Technically, it&rsquo;s impressive.<br>
Conceptually, it&rsquo;s clean.</p>
<p>But meanwhile, the client is still waiting for the button to work.</p>
<p>The client doesn&rsquo;t see:</p>
<ul>
<li>your refined dependency injection,</li>
<li>your well-placed design patterns,</li>
<li>your exemplary variable names.</li>
</ul>
<p>They see:</p>
<blockquote>
<p>“Does it work?”</p>
</blockquote>
<hr>
<h2 id="on-the-other-hand-the-cowboy-coder">On the Other Hand: the Cowboy Coder</h2>
<p>Then there&rsquo;s the other profile.</p>
<p>The one who writes fast.<br>
Sometimes dirty.<br>
Sometimes redundant.<br>
But functional.</p>
<p>The product ships.<br>
The client is happy.<br>
The business moves forward.</p>
<p>And often, it&rsquo;s that code that brings in the money.</p>
<hr>
<h2 id="the-truth-lies-in-between">The Truth Lies in Between</h2>
<p>Saying that only the result matters would be wrong.<br>
Technical debt exists. It&rsquo;s real. It costs a lot.</p>
<p>But hiding behind “the perfect architecture” to delay delivery is also a form of avoidance.</p>
<p><img src="https://perrot.pt/posts/the-client-doesnt-pay-for-your-perfect-architecture/pareto.png" alt="Pareto principle, the 80/20 rule: effort versus results" width="800" height="642" loading="lazy" decoding="async"></p>
<p>The Pareto principle (which I&rsquo;ve loved since my teens) is useful here:</p>
<blockquote>
<p>20% of well-placed effort produces 80% of the value.</p>
</blockquote>
<p>The extra 80% of effort to make the code “academically pure” doesn&rsquo;t always produce 80% of additional value.</p>
<p>Sometimes, it produces 5% of intellectual comfort.</p>
<hr>
<h2 id="what-the-client-really-buys">What the Client Really Buys</h2>
<p>A client doesn&rsquo;t buy:</p>
<ul>
<li>a pattern,</li>
<li>an elegant structure,</li>
<li>a well-organized repository.</li>
</ul>
<p>They buy:</p>
<ul>
<li>a solution to a problem,</li>
<li>time savings,</li>
<li>cost reduction,</li>
<li>a competitive advantage.</li>
</ul>
<p>Architecture is a means.<br>
Not an end.</p>
<hr>
<h2 id="but-be-careful-functional-is-not-an-excuse">But Be Careful: “Functional” Is Not an Excuse</h2>
<p>The opposite argument is just as dangerous.</p>
<p>“It works” is not a sufficient argument if:</p>
<ul>
<li>nobody can maintain the code,</li>
<li>every change breaks something else,</li>
<li>scalability is impossible,</li>
<li>refactoring becomes prohibitively expensive.</li>
</ul>
<p>Throwaway code in production is a time bomb.</p>
<p>True maturity means knowing where to put the effort.</p>
<hr>
<h2 id="a-simple-rule">A Simple Rule</h2>
<p>Before adding a layer of abstraction, ask yourself three questions:</p>
<ol>
<li>Does this need really exist today?</li>
<li>Does it simplify future maintenance?</li>
<li>Will the client get measurable value from it?</li>
</ol>
<p>If the answer is no to all three…<br>
You may be coding for yourself, not for the product.</p>
<hr>
<h2 id="what-ive-learned-over-time">What I&rsquo;ve Learned Over Time</h2>
<p>A good developer is not the one who writes the most elegant code.</p>
<p>It&rsquo;s the one who knows how to:</p>
<ul>
<li>ship fast when needed,</li>
<li>structure when necessary,</li>
<li>simplify instead of complicating,</li>
<li>arbitrate between speed and cleanliness,</li>
<li>and above all, stop the architectural masturbation.</li>
</ul>
<p>Perfect code doesn&rsquo;t exist.<br>
Useful code does.</p>
<p>And in a project:</p>
<blockquote>
<p>usefulness must always come before technical ego.</p>
</blockquote>
<p>Stop reinventing the wheel.</p>
<p>And for God&rsquo;s sake, stop reinventing the axe that will chop the wood to reinvent that goddamn wheel.</p>
<p>Churn out code that works, not code that shines.</p>
]]></content:encoded></item><item><title>Why is os.scandir() significantly faster than os.walk() in Python?</title><link>https://perrot.pt/posts/why-os-scandir-is-significantly-faster-than-os-walk-in-python/</link><pubDate>Thu, 22 Jan 2026 18:42:01 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Dev</category><guid isPermaLink="true">https://perrot.pt/posts/why-os-scandir-is-significantly-faster-than-os-walk-in-python/</guid><description>A real-world benchmark of os.walk() versus os.scandir() in Python: roughly twice as fast, why, and when the difference really matters.</description><content:encoded><![CDATA[<h3 id="feedback-from-a-simple-benchmark-in-a-real-world-environment">Feedback from a simple benchmark in a real-world environment</h3>
<p>When developing synchronization, indexing or file-processing tools (backup, directory scanning, etc.), recursive traversal of the file system quickly becomes a <strong>critical performance point</strong>.</p>
<p>In our tests, we wanted to concretely compare two common approaches in Python:</p>
<ul>
<li><code>os.walk()</code> (historical method, widely used)</li>
<li><code>os.scandir()</code> (more recent, lower-level API)</li>
</ul>
<p>The goal: <strong>a simple benchmark, without artificial optimization</strong>, run on real folders, with a few hundred to several tens of thousands of files.</p>
<hr>
<h2 id="the-benchmark">The benchmark</h2>
<p>The test consists of:</p>
<ul>
<li>recursively traversing a folder</li>
<li>counting the total number of files</li>
<li>measuring the execution time</li>
</ul>
<p>Same logic, same machine, same business code.<br>
The only difference: the traversal method.</p>
<hr>
<h2 id="observed-results">Observed results</h2>
<p>Here are some representative excerpts from the tests carried out:</p>
<table>
  <thead>
      <tr>
          <th>Number of files</th>
          <th><code>os.walk()</code></th>
          <th><code>os.scandir()</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>67 files</td>
          <td>0.036 s</td>
          <td>0.019 s</td>
      </tr>
      <tr>
          <td>195 files</td>
          <td>0.180 s</td>
          <td>0.100 s</td>
      </tr>
      <tr>
          <td>8,852 files</td>
          <td>10.28 s</td>
          <td>5.54 s</td>
      </tr>
      <tr>
          <td>19,381 files</td>
          <td>18.25 s</td>
          <td>9.95 s</td>
      </tr>
  </tbody>
</table>
<p>👉 <strong>In every case, <code>os.scandir()</code> is about 2× faster.</strong></p>
<p>And the larger the tree, the more significant the gap becomes.</p>
<p><img src="https://perrot.pt/posts/why-os-scandir-is-significantly-faster-than-os-walk-in-python/benchmark.png" alt="Terminal output of the os.walk versus os.scandir benchmark" width="488" height="520" loading="lazy" decoding="async"></p>
<hr>
<h2 id="why-this-difference">Why this difference?</h2>
<h3 id="1-the-hidden-cost-of-stat">1. The hidden cost of <code>stat()</code></h3>
<p><code>os.walk()</code> relies on a higher-level abstraction.<br>
When it traverses files, Python makes <strong>many <code>stat()</code> system calls</strong> to determine whether an entry is a file or a folder.</p>
<p>These calls are:</p>
<ul>
<li>costly on disk</li>
<li>even slower on <strong>network shares (SMB, NFS, NAS)</strong></li>
<li>multiplied by the total number of entries</li>
</ul>
<h3 id="2-osscandir-works-at-a-lower-level">2. <code>os.scandir()</code> works at a lower level</h3>
<p><code>os.scandir()</code> directly uses the information returned by the file system (via <code>dirent</code> on Linux).</p>
<p>Result:</p>
<ul>
<li>the type (file/folder) is often already known</li>
<li>no systematic <code>stat()</code> needed</li>
<li>far fewer kernel ↔ userland transitions</li>
</ul>
<p>This is exactly what explains the <strong>massive performance gain</strong>.</p>
<hr>
<h2 id="but-doesnt-oswalk-already-use-scandir">&ldquo;But doesn&rsquo;t <code>os.walk()</code> already use scandir?&rdquo;</h2>
<p>Yes… <strong>internally</strong>, since Python 3.5.</p>
<p>But:</p>
<ul>
<li><code>os.walk()</code> remains a general-purpose API</li>
<li>it generates full tuples (<code>root</code>, <code>dirs</code>, <code>files</code>)</li>
<li>it does more work than necessary in simple cases</li>
</ul>
<p>👉 When you simply want to <strong>traverse quickly</strong>, using <code>os.scandir()</code> directly remains unbeatable.</p>
<hr>
<h2 id="why-choose-python-despite-lower-performance-than-some-languages">Why choose Python, despite lower performance than some languages?</h2>
<p>It is widely accepted that Python is, on a purely technical level, slower than compiled languages like C, C++ or Rust. This reality is not, however, a flaw in itself, but rather a <strong>deliberate trade-off</strong>. Choosing a language is never limited to raw performance alone: it involves considerations of budget, complexity, maintainability and long-term technical debt.</p>
<p>Python stands out above all for its <strong>simplicity of design and readability</strong>. Clearer code reduces development time, makes code reviews easier, limits errors and allows teams to ramp up faster. For the same budget, this translates into more features delivered, faster, and with a significantly lower project risk.</p>
<p>Moreover, not all projects require maximum optimization from the outset. In many cases, performance is not the main limiting factor, or can be improved locally, only where it is needed. Python precisely allows this progressive approach: start simple, then optimize in a targeted way, without premature over-engineering.</p>
<p>Finally, technical debt is often underestimated when more complex technologies are favored from the start. Very high-performing languages that demand more expertise can generate high costs in maintenance, recruitment and functional evolution. Python, by reducing this initial complexity, offers a <strong>lasting balance</strong> between efficiency, scalability and cost control.</p>
<p>In short, a bit of love for Python 😉</p>
<hr>
<h2 id="critical-use-cases-where-the-difference-matters">Critical use cases where the difference matters</h2>
<p>In our projects, this kind of gain is far from theoretical. It is decisive for:</p>
<ul>
<li>🔄 <strong>synchronization tools</strong></li>
<li>📂 <strong>standalone folder scanners</strong> with several tens of thousands of files</li>
<li>🧾 <strong>data processing pipelines</strong></li>
<li>🌐 <strong>file servers</strong></li>
<li>🧠 <strong>indexing engines</strong></li>
</ul>
<p>Halving scan time means:</p>
<ul>
<li>less CPU load</li>
<li>fewer disk I/O operations</li>
<li>less perceived latency</li>
<li>and better overall scalability</li>
</ul>
<hr>
<h2 id="recommendation">Recommendation</h2>
<p>👉 <strong>For any new Python development involving recursive file traversal:</strong></p>
<ul>
<li>prefer <code>os.scandir()</code> with explicit recursion</li>
<li>reserve <code>os.walk()</code> for simple or non-critical scripts</li>
</ul>
<p>This choice alone can turn an &ldquo;acceptable&rdquo; script into a <strong>truly industrial</strong> tool.</p>
<hr>
<p>If you work on <strong>performance, storage, automation or large-scale file processing</strong> issues, I can support you from architecture all the way to low-level optimization.</p>
<p>📩 <strong>Got a project?</strong> <a href="https://perrot.pt/contact/">Get in touch</a>: <strong>Arthur Perrot</strong>, infrastructure &amp; cybersecurity expert.</p>
]]></content:encoded></item><item><title>Why your enterprise hard drive won't start: the 3.3 V explanation and the Kapton fix</title><link>https://perrot.pt/posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/</link><pubDate>Sun, 26 Oct 2025 20:05:39 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><guid isPermaLink="true">https://perrot.pt/posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/</guid><description>An enterprise hard drive won't spin up on a recent PC power supply? The SATA 3.3 PWDIS pin is to blame, and a piece of Kapton tape fixes it.</description><content:encoded><![CDATA[<p>We like to get to the bottom of things.</p>
<p>When I install a hard drive in our infrastructure that stubbornly refuses to spin, I don&rsquo;t settle for a quick diagnosis. And in many cases — especially with <strong>Seagate Exos</strong>, <strong>HGST Ultrastar</strong> or <strong>Toshiba Enterprise</strong> — the cause isn&rsquo;t the drive itself, but… <strong>the SATA power connector</strong>.</p>
<hr>
<h2 id="-the-culprit-the-sata-33-standard-and-the-infamous-33-v-pin">🔍 The culprit: the SATA 3.3 standard and the infamous 3.3 V pin</h2>
<p>Since the <strong>SATA 3.3</strong> standard, three small pins at the end of the power connector have had their role changed.</p>
<table>
  <thead>
      <tr>
          <th>Pin</th>
          <th>Voltage</th>
          <th>Function (SATA 3.3)</th>
          <th>Effect</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>3.3 V</td>
          <td>Power</td>
          <td>—</td>
      </tr>
      <tr>
          <td>2</td>
          <td>3.3 V</td>
          <td>Power</td>
          <td>—</td>
      </tr>
      <tr>
          <td>3</td>
          <td>3.3 V</td>
          <td><strong>PWDIS (Power Disable)</strong></td>
          <td>Allows the drive to be disabled remotely</td>
      </tr>
  </tbody>
</table>
<p><strong>Enterprise</strong> drives, often designed for Dell, HP or Supermicro servers, interpret <strong>pin 3</strong> as a disable command.<br>
👉 Result: <strong>if your PC power supply sends 3.3 V on this line</strong>, the drive thinks it&rsquo;s being told to shut down. It doesn&rsquo;t spin, doesn&rsquo;t heat up, and stays invisible in the BIOS.</p>
<p><img src="https://perrot.pt/posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/sata-pinout.png" alt="SATA connector of a hard drive with the 3.3 V, 5 V and 12 V pins annotated" width="800" height="523" loading="lazy" decoding="async"></p>
<hr>
<h2 id="-why-some-power-supplies-cause-problems">⚡ Why some power supplies cause problems</h2>
<p>Modern power supplies (such as the <strong>Corsair CV550</strong>, BeQuiet!, Cooler Master or consumer Seasonic) <strong>strictly follow the SATA 3.3 standard</strong>.<br>
They therefore deliver the <strong>3.3 V</strong> (the famous <strong>orange</strong> wire) to the SATA connector pins.<br>
On a standard drive, no problem.<br>
But on an <strong>Enterprise drive</strong>, this voltage activates the <strong>PWDIS</strong> signal, which blocks any startup.</p>
<p>And even if you <strong>remove the orange wire</strong>, some models still cause problems:</p>
<ul>
<li>the SATA connector can <strong>make mechanical contact</strong> with pins 1–3,</li>
<li>an <strong>internal rail</strong> of the power supply can feed back a faint stray voltage,</li>
<li>or the internal wiring keeps the pin active despite no visible wire.</li>
</ul>
<p>👉 The drive therefore stays disabled, even without a visible orange wire.</p>
<hr>
<h2 id="-known-and-tested-solutions">🧠 Known (and tested) solutions</h2>
<table>
  <thead>
      <tr>
          <th>Method</th>
          <th>Description</th>
          <th>Reliability</th>
          <th>Drawback</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Molex → SATA adapter</strong></td>
          <td>The Molex doesn&rsquo;t carry 3.3 V (only 5 V &amp; 12 V).</td>
          <td>⭐⭐⭐⭐</td>
          <td>Requires an adapter.</td>
      </tr>
      <tr>
          <td><strong>&ldquo;No 3.3 V&rdquo; SATA extension</strong></td>
          <td>Cable without the orange line.</td>
          <td>⭐⭐⭐</td>
          <td>Varies by model.</td>
      </tr>
      <tr>
          <td><strong>Kapton tape over pins 1–3</strong></td>
          <td>Physical isolation of the first 3 pins of the SATA connector.</td>
          <td>⭐⭐⭐⭐⭐</td>
          <td>You have to aim carefully!</td>
      </tr>
      <tr>
          <td><strong>Classic tape / Teflon</strong></td>
          <td>Insulators, but neither durable nor heat-resistant.</td>
          <td>⭐</td>
          <td>Temporary use only.</td>
      </tr>
  </tbody>
</table>
<hr>
<p><img src="https://perrot.pt/posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/kapton-test_hu_2a63f23ee51d0d3.webp" srcset="/posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/kapton-test_hu_80a68fbe0b5307d4.webp 1000w, /posts/why-your-enterprise-hard-drive-wont-start-the-3-3-v-explanation-and-the-kapton-fix/kapton-test_hu_2a63f23ee51d0d3.webp 1350w" sizes="(min-width: 800px) 732px, 100vw" alt="SATA connector of a hard drive with yellow tape over the first pins" width="1350" height="1800" loading="lazy" decoding="async"></p>
<blockquote>
<p>Okay, this isn&rsquo;t actually Kapton, but it was just for testing!</p>
</blockquote>
<h2 id="-why-kapton-is-the-best-solution">🟡 Why <strong>Kapton</strong> is the best solution</h2>
<p>Kapton (polyimide) is an insulating tape designed for electronics:</p>
<ul>
<li>it resists heat (up to +260 °C),</li>
<li>it leaves <strong>no residue</strong> when removed,</li>
<li>it&rsquo;s <strong>ultra-thin</strong>, so the SATA connector slides on without forcing.</li>
</ul>
<p>To neutralize the 3.3 V line, simply:</p>
<ol>
<li>Cut a small rectangle (~5 × 3 mm),</li>
<li>Stick it over the <strong>first 3 pins</strong> on the left of the SATA connector (gold-contact side),</li>
<li>Reconnect the power cable.</li>
</ol>
<p>Your drive will start up <strong>instantly</strong> — with no permanent change to the wiring.</p>
<hr>
<h2 id="-summary">🧩 Summary</h2>
<table>
  <thead>
      <tr>
          <th>Symptom</th>
          <th>Probable cause</th>
          <th>Solution</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>The drive doesn&rsquo;t spin</td>
          <td>Active 3.3 V line → PWDIS blocked</td>
          <td>Kapton on pins 1–3</td>
      </tr>
      <tr>
          <td>The drive works in a server but not on a PC</td>
          <td>Server backplane cuts the 3.3 V</td>
          <td>Molex → SATA adapter</td>
      </tr>
      <tr>
          <td>The SATA cable without the orange wire changes nothing</td>
          <td>Internal rail still active</td>
          <td>Physical isolation required</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="-conclusion">🧰 Conclusion</h2>
<p>This small design detail, invisible to the naked eye, explains <strong>hundreds of false &ldquo;dead drive&rdquo; diagnoses</strong> every year.</p>
<p>Around here, we prefer understanding over replacing.<br>
So before sending back a &ldquo;dead&rdquo; drive, check your cables, your rails… and if needed, take out the <strong>Kapton</strong> tape.</p>
<blockquote>
<p>💡 Tip: if you often work with Enterprise drives, always keep a few <strong>Molex → SATA</strong> adapters and a roll of <strong>Kapton tape</strong> in your workshop.<br>
It will save you a lot of cold sweats.</p>
</blockquote>
]]></content:encoded></item><item><title>Bouygues Network Bug – WireGuard Disruption Between Public IPv6 and Fixed IPv4</title><link>https://perrot.pt/posts/bouygues-network-bug-wireguard-disruption-between-public-ipv6-and-fixed-ipv4/</link><pubDate>Wed, 22 Oct 2025 00:54:57 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Network</category><guid isPermaLink="true">https://perrot.pt/posts/bouygues-network-bug-wireguard-disruption-between-public-ipv6-and-fixed-ipv4/</guid><description>Since a firmware update, Bouygues Wi-Fi 6 Bbox routers disrupt WireGuard tunnels: symptoms, likely cause (reduced MTU) and the MTU = 1380 fix.</description><content:encoded><![CDATA[<h3 id="-observations-since-mid-october-2025">📅 Observations since mid-October 2025</h3>
<p>A small French company that is a Horarys client, a consulting firm with about ten employees working partly remotely, contacted us: for roughly two weeks, several of its users connected via Bouygues Telecom Bbox Wi-Fi 6 or Wi-Fi 6E boxes had been experiencing significant instability on their <strong>WireGuard</strong> VPN connections. We helped them diagnose the problem.<br>
The issue mainly affects the following configurations:</p>
<ul>
<li>Tunnel <strong>between a client on a public IPv6 address (Bbox)</strong> and a <strong>server on a fixed IPv4 address</strong></li>
<li>Access via a <strong>WireGuard bridge / routed local network</strong></li>
<li><strong>UDP carrying TCP traffic (SMB, SSH, RDP, HTTPS)</strong></li>
</ul>
<hr>
<h3 id="-observed-symptoms">🚨 Observed symptoms</h3>
<ul>
<li><strong>Ping</strong> works (ICMP not affected)</li>
<li><strong>RDP, SSH, SMB</strong> or <strong>web interfaces</strong> connect, then freeze or disconnect</li>
<li><strong>Heavy packet loss</strong>, abnormal latency on heavy TCP flows</li>
<li>No apparent firewall blocking, and no TCP reset</li>
<li>The problem is limited to <strong>clients behind Bouygues Bboxes</strong>, while other connections (Free, Orange, SFR, Bouygues 5G, etc.) remain stable</li>
</ul>
<hr>
<h3 id="-probable-cause">🔍 Probable cause</h3>
<p>The network analyses carried out during the diagnosis indicate an <strong>effective reduction of the MTU</strong> on the WAN interfaces of Bouygues Bbox Wi-Fi 6 boxes since the latest firmware update.<br>
This update also seems to <strong>force the activation of IPv6 (SLAAC / DHCPv6-PD)</strong>, creating an IPv6/IPv4 mix that is not transparent for WireGuard tunnels.</p>
<p>As a result:</p>
<ul>
<li>WireGuard UDP packets exceeding <strong>~1408 bytes</strong> are fragmented or lost</li>
<li><strong>Ping (small packets)</strong> continues to work</li>
<li><strong>Encapsulated TCP flows</strong> become unstable, especially on connections to fixed IPv4 servers.</li>
</ul>
<hr>
<h3 id="-temporary-workaround">🧰 Temporary workaround</h3>
<p>Bouygues Telecom has not yet published any official communication, but the solution is simple and effective:</p>
<h4 id="-on-the-wireguard-client-behind-the-bbox">✅ On the WireGuard client (behind the Bbox)</h4>
<p>Edit the configuration file:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Interface]</span>
</span></span><span class="line"><span class="cl"><span class="na">MTU</span> <span class="o">=</span> <span class="s">1380</span></span></span></code></pre></div></div>
<p>Then restart the tunnel.</p>
<p>This single line forces the maximum WireGuard packet size to a threshold compatible with the new Bouygues MTU (~1408 effective bytes), immediately eliminating the losses and slowdowns.</p>
<h4 id="-in-addition">💡 In addition</h4>
<ul>
<li><strong>Disable IPv6</strong> on the Bbox (or on the client) if possible (not possible on consumer Bboxes; no feedback on the business side)</li>
<li>Check that the <strong>DNS used</strong> is IPv4</li>
<li>Do <strong>not change the MTU on the server side</strong> (pfSense or Proxmox) if the other clients work correctly</li>
</ul>
<hr>
<h3 id="-typical-example">🧩 Typical example</h3>
<p>In this case, a client machine with the WireGuard address <code>10.61.0.10</code> behind a Bouygues Bbox trying to reach a Proxmox server at <code>10.172.41.1</code> saw:</p>
<ul>
<li>Stable ping</li>
<li>but the web interface (<code>:8006</code>) and SMB frozen.</li>
</ul>
<p>After adding <code>MTU = 1380</code>, traffic immediately became smooth again.</p>
<hr>
<h3 id="-summary">🧠 Summary</h3>
<table>
  <thead>
      <tr>
          <th>Element</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ISP concerned</td>
          <td>Bouygues Telecom (Bbox Wi-Fi 6 / Wi-Fi 6E)</td>
      </tr>
      <tr>
          <td>Impact</td>
          <td>Heavy packet loss on WireGuard IPv6→IPv4 tunnels</td>
      </tr>
      <tr>
          <td>Probable cause</td>
          <td>MTU reduction and automatic IPv6 activation via firmware</td>
      </tr>
      <tr>
          <td>Solution</td>
          <td>Force <code>MTU = 1380</code> on the WireGuard client</td>
      </tr>
      <tr>
          <td>Bouygues fix</td>
          <td>Not announced to date</td>
      </tr>
  </tbody>
</table>
<hr>
<h3 id="-our-recommendation">🛡️ Our recommendation</h3>
<p>I advise all my clients using WireGuard tunnels on Bouygues infrastructure to:</p>
<ol>
<li><strong>check tunnel stability with <code>ping -f -l 1472</code> (Windows) or <code>ping -M do -s 1472</code> (Linux)</strong>,</li>
<li><strong>adjust the MTU to 1380</strong> if necessary,</li>
<li><strong>temporarily disable IPv6</strong> if instability persists.</li>
</ol>
<p>I&rsquo;m keeping a close eye on this behavior and will update this note as soon as an official Bouygues fix or a firmware change is confirmed.</p>
<hr>
<p>💬 <strong>Need assistance?</strong><br>
I can audit your WireGuard tunnel or your multi-site interconnection.<br>
👉 <a href="https://perrot.pt/contact/">Get in touch</a>: <strong>Arthur Perrot</strong>, infrastructure &amp; cybersecurity expert.</p>
]]></content:encoded></item><item><title>Understanding throughput to size your network</title><link>https://perrot.pt/posts/understanding-throughput-to-size-your-network/</link><pubDate>Mon, 22 Sep 2025 22:31:46 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Network</category><category>SysAdmin</category><guid isPermaLink="true">https://perrot.pt/posts/understanding-throughput-to-size-your-network/</guid><description>Bits or bytes, disk or network: how to compare throughput, estimate how long a backup takes and size a network without a bottleneck.</description><content:encoded><![CDATA[<p>We often see infrastructures that are well equipped with servers and storage… but held back by their network. Understanding <strong>throughput</strong> is essential to avoid bottlenecks, especially during massive backups or data transfers between sites.</p>
<hr>
<h2 id="bits-bytes-the-basics">Bits, bytes: the basics</h2>
<ul>
<li><strong>1 byte (B)</strong> = <strong>8 bits (b)</strong>.</li>
<li>Network speeds are generally expressed in <strong>bits per second</strong> (e.g. 1 Gb/s).</li>
<li>Disk and file transfer speeds are expressed in <strong>bytes per second</strong> (e.g. 150 MB/s).</li>
</ul>
<p>👉 Result: <strong>1 Gb/s ≈ 125 MB/s</strong> in practice.</p>
<hr>
<h2 id="throughput-comparison-table">Throughput comparison table</h2>
<p>Here is a simple table to better visualize the typical speeds of disks and networks, with their English and French equivalents:</p>
<table>
  <thead>
      <tr>
          <th>Technology</th>
          <th>Typical throughput</th>
          <th>Equivalent in MB/s</th>
          <th>English term</th>
          <th>French term</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>HDD 7200 rpm (14 TB)</strong></td>
          <td>1.2–1.6 Gbit/s</td>
          <td>150–200 MB/s</td>
          <td>Hard Disk Drive</td>
          <td>Disque dur</td>
      </tr>
      <tr>
          <td><strong>SATA 6G SSD</strong></td>
          <td>4–4.4 Gbit/s</td>
          <td>500–550 MB/s</td>
          <td>SATA SSD</td>
          <td>SSD SATA</td>
      </tr>
      <tr>
          <td><strong>PCIe 3.0 x4 NVMe SSD</strong></td>
          <td>24–28 Gbit/s</td>
          <td>3000–3500 MB/s</td>
          <td>NVMe SSD</td>
          <td>SSD NVMe</td>
      </tr>
      <tr>
          <td><strong>PCIe 4.0 x4 NVMe SSD</strong></td>
          <td>40–56 Gbit/s</td>
          <td>5000–7000 MB/s</td>
          <td>Gen4 NVMe SSD</td>
          <td>SSD NVMe Gen4</td>
      </tr>
      <tr>
          <td><strong>1 GbE network</strong></td>
          <td>1 Gbit/s</td>
          <td>~125 MB/s</td>
          <td>1 Gigabit Ethernet</td>
          <td>Réseau 1 Gigabit</td>
      </tr>
      <tr>
          <td><strong>2.5 GbE network</strong></td>
          <td>2.5 Gbit/s</td>
          <td>~312 MB/s</td>
          <td>2.5 Gigabit Ethernet</td>
          <td>Réseau 2,5 Gigabit</td>
      </tr>
      <tr>
          <td><strong>10 GbE network</strong></td>
          <td>10 Gbit/s</td>
          <td>~1250 MB/s</td>
          <td>10 Gigabit Ethernet</td>
          <td>Réseau 10 Gigabit</td>
      </tr>
      <tr>
          <td><strong>40 GbE network</strong></td>
          <td>40 Gbit/s</td>
          <td>~5000 MB/s</td>
          <td>40 Gigabit Ethernet</td>
          <td>Réseau 40 Gigabit</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="concrete-example-a-1-tb-backup">Concrete example: a 1 TB backup</h2>
<p>Let&rsquo;s imagine a <strong>1 TB</strong> (≈ 1000 GB) backup to transfer to a backup server. Here is how long it takes depending on the network:</p>
<ul>
<li><strong>1 GbE network</strong> (125 MB/s max):<br>
1000 GB ÷ 125 MB/s ≈ <strong>2h15</strong></li>
<li><strong>10 GbE network</strong> (1250 MB/s max):<br>
1000 GB ÷ 1250 MB/s ≈ <strong>13 minutes</strong></li>
<li><strong>40 GbE network</strong> (5000 MB/s max):<br>
1000 GB ÷ 5000 MB/s ≈ <strong>3 minutes</strong></li>
</ul>
<p>👉 You can see the problem: on <strong>1 GbE</strong>, a simple backup can take <strong>hours</strong>, and during that time your network is saturated.</p>
<hr>
<h2 id="the-bottleneck">The bottleneck</h2>
<p>A key principle to remember: <strong>the final speed always depends on the slowest link</strong>.</p>
<p>Let&rsquo;s take an example: you need to back up <strong>4 TB of data</strong>.</p>
<ul>
<li>With <strong>NVMe SSDs</strong> capable of 3500 MB/s, a pure disk-to-disk copy could theoretically be done in <strong>under 20 minutes</strong>.</li>
<li>But if your network is only <strong>1 GbE</strong> (≈ 125 MB/s real), the transfer drops to:<br>
4000 GB ÷ 125 MB/s ≈ <strong>9 hours</strong>.</li>
<li>With a <strong>10 GbE network</strong> (≈ 1250 MB/s), the same backup would only take:<br>
4000 GB ÷ 1250 MB/s ≈ <strong>53 minutes</strong>.</li>
</ul>
<p>👉 The takeaway: your disks can be ultra fast, but if the network is too slow, it will <strong>bottleneck overall performance</strong>.</p>
<hr>
<h2 id="how-to-size-intelligently">How to size intelligently?</h2>
<ol>
<li><strong>Assess your data flows</strong>: how many TB of backup each night, how many VMs replicated, what transfer times are acceptable.</li>
<li><strong>Compare disk and network</strong>: make sure the network doesn&rsquo;t throttle the disks, and vice versa.</li>
<li><strong>Plan for headroom</strong>: with data growth, it&rsquo;s better to slightly oversize.</li>
<li><strong>Segment your usage</strong>: separate production traffic, backups and replicas to avoid overall saturation.</li>
</ol>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Properly sizing <strong>network throughput</strong> is just as critical as choosing the right servers.<br>
I help companies <strong>audit, optimize and modernize their network and storage infrastructures</strong>. The goal: ensure your backups complete on time, without saturating your critical flows.</p>
<p>📩 <strong>Unsure about sizing your network?</strong><br>
👉 <a href="https://perrot.pt/contact/">Get in touch</a>: <strong>Arthur Perrot</strong>, infrastructure &amp; cybersecurity expert.</p>
]]></content:encoded></item><item><title>Understanding the difference between useradd and adduser on Linux</title><link>https://perrot.pt/posts/understanding-the-difference-between-useradd-and-adduser-on-linux/</link><pubDate>Wed, 10 Sep 2025 14:07:05 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><guid isPermaLink="true">https://perrot.pt/posts/understanding-the-difference-between-useradd-and-adduser-on-linux/</guid><description>useradd is the low-level tool, adduser an interactive Perl wrapper specific to Debian: practical differences and which one to pick.</description><content:encoded><![CDATA[<p>When administering a Linux system, you often need to create new users. Two commands exist for this: <strong><code>useradd</code></strong> and <strong><code>adduser</code></strong>. At first glance they seem equivalent, but their origin and behavior differ noticeably.</p>
<h2 id="1-useradd-the-native-command">1. <code>useradd</code>: the native command</h2>
<p><code>useradd</code> is one of the <strong>low-level user account management tools</strong> provided by the <code>passwd</code> package (often coming from the <a href="https://github.com/shadow-maint/shadow">shadow-utils</a> project in modern distributions).</p>
<ul>
<li><strong>Role</strong>: create a user account by directly manipulating system files such as <code>/etc/passwd</code>, <code>/etc/shadow</code>, <code>/etc/group</code> and <code>/etc/skel</code>.</li>
<li><strong>Behavior</strong>: by default, <code>useradd</code> is quite strict and minimal. If no option is passed, the user can be created <strong>without a home directory</strong> and without a shell defined.</li>
<li><strong>Syntax</strong>: <code>useradd -m -s /bin/bash newuser</code> Here, <code>-m</code> forces the creation of the home directory and <code>-s</code> sets the default shell.</li>
</ul>
<p>In short, <code>useradd</code> is a <strong>low-level</strong> tool, meant to be used in scripts or by experienced administrators who know exactly which parameters to provide.</p>
<hr>
<h2 id="2-adduser-the-friendly-wrapper">2. <code>adduser</code>: the friendly wrapper</h2>
<p><code>adduser</code> is not a native command of the kernel or of the base utilities, but a <strong>Perl script</strong> (in Debian/Ubuntu and derivatives). Its goal is to make the experience simpler and more interactive.</p>
<ul>
<li><strong>Role</strong>: provide a <strong>high-level interface</strong> to <code>useradd</code>.</li>
<li><strong>How it works</strong>: it acts as a <strong>wrapper</strong>: it calls <code>useradd</code> (and sometimes <code>usermod</code>) in the background, but adds validation logic and interaction with the administrator.</li>
<li><strong>Interaction</strong>: when you type <code>adduser newuser</code>, the script asks you questions: password, full name, additional information… then automatically creates the home directory, copies the files from <code>/etc/skel</code>, sets permissions, etc.</li>
</ul>
<h3 id="why-perl">Why Perl?</h3>
<p>Historically, in Debian and its derivatives, <code>adduser</code> was written in <strong>Perl</strong> in order to quickly benefit from a powerful and portable scripting language to add this ergonomic layer on top of the existing system tools.</p>
<ul>
<li>It made conditional and interactive logic possible, which would be harder to maintain in pure shell.</li>
<li>With Perl, string handling, interactive forms and configuration are simpler.</li>
</ul>
<hr>
<h2 id="3-practical-differences-between-useradd-and-adduser">3. Practical differences between <code>useradd</code> and <code>adduser</code></h2>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th><code>useradd</code></th>
          <th><code>adduser</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Origin</td>
          <td>Low-level tool (<code>shadow-utils</code>)</td>
          <td>Perl script (Debian/Ubuntu)</td>
      </tr>
      <tr>
          <td>Home directory</td>
          <td>Not created by default</td>
          <td>Always created</td>
      </tr>
      <tr>
          <td>Default shell</td>
          <td>May be <code>/bin/sh</code> or undefined</td>
          <td>Set automatically (often <code>/bin/bash</code>)</td>
      </tr>
      <tr>
          <td>Interaction</td>
          <td>None, requires all options</td>
          <td>Asks questions (password, info)</td>
      </tr>
      <tr>
          <td>Portability</td>
          <td>Present on all distros</td>
          <td>Specific to Debian-based distros (Ubuntu, Mint, etc.)</td>
      </tr>
      <tr>
          <td>Target</td>
          <td>Scripts and expert admins</td>
          <td>Administrators looking for simplicity</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="4-conclusion-which-one-to-use">4. Conclusion: which one to use?</h2>
<ul>
<li>On <strong>Debian/Ubuntu</strong> and derivative distributions, <strong><code>adduser</code></strong> is recommended for most cases, as it is safer and prevents oversights (home directory, shell, etc.).</li>
<li>On other systems like <strong>Red Hat, CentOS, Fedora</strong>, only the <strong><code>useradd</code></strong> command is available — which reflects its nature as a universal tool.</li>
</ul>
<p>In practice, <strong><code>adduser</code> is just a Perl wrapper around <code>useradd</code></strong>, but it illustrates well the Debian philosophy: making administration more accessible while keeping the power of Unix tools in the background.</p>
<hr>
<p>👉 When we automate account management in our client infrastructures, we favor <code>useradd</code> in scripts (predictable, universal), and <code>adduser</code> for daily manual administration (ergonomic, secure).</p>
]]></content:encoded></item><item><title>VS Code Config: Enforcing a Coding Style with Rulers</title><link>https://perrot.pt/posts/vs-code-config-enforcing-a-coding-style-with-rulers/</link><pubDate>Sun, 24 Aug 2025 01:29:23 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Dev</category><guid isPermaLink="true">https://perrot.pt/posts/vs-code-config-enforcing-a-coding-style-with-rulers/</guid><description>A minimal VS Code settings.json: an 80-column ruler, visible whitespace and fixed indentation to enforce a shared coding style across the team.</description><content:encoded><![CDATA[<h2 id="why-a-ruler">Why a Ruler?</h2>
<p>In a team, respecting a <strong>code style</strong> is essential.<br>
One of VS Code&rsquo;s simple tools is the <strong>ruler</strong>: a vertical line displayed in the editor at a given column (e.g. 80 characters).</p>
<p>It helps you keep track of <strong>how far you write on each line</strong>: better readability, consistency between developers, following best practices.</p>
<hr>
<h2 id="minimal-configuration-example">Minimal Configuration Example</h2>
<p>Here is a <code>settings.json</code> configuration for VS Code, focused on the essentials:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">json</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;editor.rulers&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">80</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;workbench.startupEditor&#34;</span><span class="p">:</span> <span class="s2">&#34;newUntitledFile&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;git.autofetch&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;editor.detectIndentation&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;editor.renderWhitespace&#34;</span><span class="p">:</span> <span class="s2">&#34;all&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;workbench.panel.defaultLocation&#34;</span><span class="p">:</span> <span class="s2">&#34;right&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;terminal.integrated.cursorStyle&#34;</span><span class="p">:</span> <span class="s2">&#34;line&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;terminal.integrated.fontFamily&#34;</span><span class="p">:</span> <span class="s2">&#34;monospace&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;window.menuBarVisibility&#34;</span><span class="p">:</span> <span class="s2">&#34;toggle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;diffEditor.ignoreTrimWhitespace&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<hr>
<h2 id="quick-explanations">Quick Explanations</h2>
<ol>
<li><strong><code>&quot;editor.rulers&quot;: [80]</code></strong><br>
Displays a vertical guide at column 80 → ideal for keeping code readable and consistent.</li>
<li><strong><code>&quot;workbench.startupEditor&quot;: &quot;newUntitledFile&quot;</code></strong><br>
VS Code opens an empty file directly on startup → saves time, you start coding right away.</li>
<li><strong><code>&quot;git.autofetch&quot;: true</code></strong><br>
Automatically updates remote Git information → you always see the real state of the project without a manual fetch.</li>
<li><strong><code>&quot;editor.detectIndentation&quot;: false</code></strong><br>
Prevents VS Code from trying to “guess” indentation → the editor follows your rules, not those detected in the file.</li>
<li><strong><code>&quot;editor.renderWhitespace&quot;: &quot;all&quot;</code></strong><br>
Visually displays spaces and tabs → avoids invisible style errors.</li>
<li><strong><code>&quot;workbench.panel.defaultLocation&quot;: &quot;right&quot;</code></strong><br>
Places the panel (console) on the right instead of at the bottom.<br>
👉 If you deliberately limit yourself to 80-column lines, it&rsquo;s better to keep more vertical space than horizontal space.<br>
This remains a <strong>personal choice</strong>; everyone adapts to their own comfort.</li>
</ol>
<hr>
<h2 id="a-clear-style-for-the-whole-team">A Clear Style, for the Whole Team</h2>
<p>Setting up these simple rules in VS Code helps you:</p>
<ul>
<li>have <strong>more readable code</strong>,</li>
<li>ensure <strong>consistency between developers</strong>,</li>
<li>and avoid invisible errors (spaces, inconsistent indentation).</li>
</ul>
<p>A small tweak to your <code>settings.json</code>, and you lay the foundations for a <strong>shared coding style</strong> across your projects.</p>
]]></content:encoded></item><item><title>VSCode Snippet: Automatically Generate Your #ifndef Guards in C/C++</title><link>https://perrot.pt/posts/vscode-snippet-automatically-generate-your-ifndef-in-c-c/</link><pubDate>Sun, 24 Aug 2025 01:25:42 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Quick tips</category><category>Dev</category><guid isPermaLink="true">https://perrot.pt/posts/vscode-snippet-automatically-generate-your-ifndef-in-c-c/</guid><description>A VS Code snippet that generates the #ifndef / #define / #endif header guards of a C or C++ file from its name, in one Tab press.</description><content:encoded><![CDATA[<h2 id="why-waste-time-rewriting-header-guards">Why waste time rewriting header guards?</h2>
<p>When coding in C or C++, every <code>.h</code> file must be protected with a <strong>header guard</strong> (<code>#ifndef</code>, <code>#define</code>, <code>#endif</code>) to prevent multiple inclusions.<br>
We have all done some mechanical <strong>copy-pasting</strong>… but VSCode lets you automate it with a <strong>very simple snippet</strong>.</p>
<h2 id="the-magic-snippet">The magic snippet</h2>
<p>Here is an example of a snippet to add to your VSCode <strong>User Snippets</strong> (menu <em>Preferences &gt; User Snippets</em> → choose <code>cpp.json</code>):</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">json</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;C C++ Header&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;scope&#34;</span><span class="p">:</span> <span class="s2">&#34;c, cpp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;prefix&#34;</span><span class="p">:</span> <span class="s2">&#34;header&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Add #ifndef, #define and #endif&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;#ifndef ${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;#define ${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;$0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;#endif&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<h2 id="how-does-it-work">How does it work?</h2>
<ul>
<li>Simply type <code>header</code> in a <code>.h</code> file</li>
<li>Press <code>Tab</code> → VSCode automatically generates:</li>
</ul>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">c</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#ifndef MYFILE_H
</span></span></span><span class="line"><span class="cl"><span class="cp">#define MYFILE_H
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Your code here
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#endif</span></span></span></code></pre></div></div>
<p>The file name is picked up automatically and converted to uppercase. 🚀</p>
<h2 id="time-and-rigor-savings">Time and rigor savings</h2>
<ul>
<li>No more writing guards by hand</li>
<li>Guaranteed uniformity across all your headers</li>
<li>Fewer multiple-inclusion errors</li>
</ul>
<p>A small snippet, a <strong>big everyday comfort</strong>.</p>
]]></content:encoded></item><item><title>Disabling Bash's annoying beep</title><link>https://perrot.pt/posts/disabling-bashs-annoying-beep/</link><pubDate>Sun, 24 Aug 2025 01:22:50 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Quick tips</category><guid isPermaLink="true">https://perrot.pt/posts/disabling-bashs-annoying-beep/</guid><description>One line in /etc/inputrc is enough to silence Bash's terminal bell, or to replace it with a visual flash.</description><content:encoded><![CDATA[<p>Who has never been surprised (or annoyed 😅) by the <strong>&ldquo;beep&rdquo;</strong> Bash makes when you type a wrong command or a typo?<br>
Good news: it is very easy to turn off this sound.</p>
<p>Just edit the file <code>/etc/inputrc</code> and add (or uncomment) the following line:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">set bell-style none</span></span></span></code></pre></div></div>
<p>Save, reload your session, and that&rsquo;s it: no more unwanted beeps! 🎉</p>
<p>👉 Bonus tip: you can also replace the beep with a <strong>visual signal</strong> using:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">ini</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">set bell-style visible</span></span></span></code></pre></div></div>
]]></content:encoded></item><item><title>Turn a folder into an instant file server</title><link>https://perrot.pt/posts/turn-a-folder-into-an-instant-file-server/</link><pubDate>Sun, 24 Aug 2025 01:22:12 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>SysAdmin</category><category>Quick tips</category><guid isPermaLink="true">https://perrot.pt/posts/turn-a-folder-into-an-instant-file-server/</guid><description>Share a folder with one command using Python's built-in HTTP server, no cloud or USB stick needed, plus the limits to keep in mind.</description><content:encoded><![CDATA[<p>Need to quickly share files in a folder, without going through a cloud or a USB stick?<br>
Python offers an ultra-fast solution:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">python3 -m http.server <span class="m">8000</span></span></span></code></pre></div></div>
<p>By default, this command starts an <strong>HTTP server</strong> that makes the current folder accessible on port 8000.<br>
Then simply connect to <code>http://your_ip:8000</code> from a browser to download your files.</p>
<p>✅ Ideal for:</p>
<ul>
<li>sharing files on a local network,</li>
<li>helping a colleague troubleshoot,</li>
<li>accessing your data from another device.</li>
</ul>
<p>⚠️ Warning: this server is basic and provides no security. Use it in a private or one-off context only.</p>
]]></content:encoded></item><item><title>Simulating keyboard typing from the clipboard</title><link>https://perrot.pt/posts/simulate-keyboard-typing-from-the-clipboard/</link><pubDate>Sun, 24 Aug 2025 01:21:19 +0000</pubDate><dc:creator>Arthur Perrot</dc:creator><category>Quick tips</category><guid isPermaLink="true">https://perrot.pt/posts/simulate-keyboard-typing-from-the-clipboard/</guid><description>Get around fields that block paste on Linux by replaying the clipboard as keystrokes with xclip and xdotool.</description><content:encoded><![CDATA[<p>Sometimes a website or application blocks <strong>copy-paste</strong> in certain text fields (for example a password or a form).<br>
Good news: on Linux, you can get around this limitation by simulating the typing of the clipboard content, as if you were typing it on the keyboard.</p>
<p>Here is the magic command:</p>
<div class="codeblock"><span class="codelang mono" aria-hidden="true">bash</span><button type="button" class="codecopy mono" data-done="Copied!" aria-label="Copy">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">xclip -selection clipboard -out <span class="p">|</span> tr <span class="se">\n</span> <span class="se">\r</span> <span class="p">|</span> xdotool selectwindow windowfocus <span class="nb">type</span> --clearmodifiers --delay <span class="m">25</span> --window %@ --file -</span></span></code></pre></div></div>
<p>👉 Quick explanation:</p>
<ul>
<li><strong>xclip</strong>: retrieves the clipboard contents.</li>
<li><strong>tr \n \r</strong>: converts line breaks to avoid bugs.</li>
<li><strong>xdotool type</strong>: simulates typing character by character, like a human on the keyboard.</li>
</ul>
<p>Handy, simple and 100% effective for saving time!</p>
]]></content:encoded></item></channel></rss>