<?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/"><channel><title>productivity · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/productivity/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Mon, 30 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/productivity/feed.xml" rel="self" type="application/rss+xml"/><item><title>Git Worktrees Changed How I Handle Branches</title><link>https://articles.wleeaf.dev/posts/git-worktrees/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/git-worktrees/</guid><description>Switching branches without stashing, losing context, or waiting for node_modules to reinstall.</description><category>git</category><category>workflow</category><category>productivity</category><content:encoded><![CDATA[<p>The worst part of working on multiple branches is the context switching. You are deep in a feature branch, someone asks you to review a hotfix, and now you have to stash your changes, switch branches, wait for your build tools to catch up, do the review, switch back, pop the stash, and hope nothing got lost.</p>
<p>Git worktrees fix this entirely: they let you check out several branches at once, each in its own directory, all sharing one repository.</p>
<h2 id="what-is-a-worktree">What Is a Worktree?<a class="anchor" href="#what-is-a-worktree" aria-label="Link to “What Is a Worktree?”">#</a></h2>
<p>A worktree is a separate working directory that shares the same <code>.git</code> data. Each worktree can have a different branch checked out simultaneously:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># You are on main in /home/user/project</span>
</span></span><span class="line"><span class="cl">git worktree add ../project-hotfix hotfix/urgent-fix</span></span></code></pre></div>
</div>
<p>Now you have two directories:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>/home/user/project          → main branch
/home/user/project-hotfix   → hotfix/urgent-fix branch</code></pre>
</div>
<p>Both share the same git history. Commits in one are visible in the other. But they have completely independent working directories.</p>
<h2 id="the-workflow">The Workflow<a class="anchor" href="#the-workflow" aria-label="Link to “The Workflow”">#</a></h2>
<p>My typical day looks like this:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Main work</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/projects/app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Someone needs a review - create a worktree</span>
</span></span><span class="line"><span class="cl">git worktree add ~/reviews/pr-142 origin/feature/new-api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Review in a completely separate directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/reviews/pr-142
</span></span><span class="line"><span class="cl">npm install  <span class="c1"># only needed first time</span>
</span></span><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Done reviewing, remove it</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/projects/app
</span></span><span class="line"><span class="cl">git worktree remove ~/reviews/pr-142</span></span></code></pre></div>
</div>
<p>The key advantage: my main working directory is completely untouched. No stashing, no branch switching, no reinstalling dependencies.</p>
<h2 id="practical-patterns">Practical Patterns<a class="anchor" href="#practical-patterns" aria-label="Link to “Practical Patterns”">#</a></h2>
<h3 id="parallel-testing">Parallel testing<a class="anchor" href="#parallel-testing" aria-label="Link to “Parallel testing”">#</a></h3>
<p>Run tests on two branches at the same time:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add /tmp/test-main main
</span></span><span class="line"><span class="cl">git worktree add /tmp/test-feature feature/new-thing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In two terminals</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/test-main <span class="o">&amp;&amp;</span> npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/test-feature <span class="o">&amp;&amp;</span> npm test</span></span></code></pre></div>
</div>
<h3 id="long-running-branches">Long-running branches<a class="anchor" href="#long-running-branches" aria-label="Link to “Long-running branches”">#</a></h3>
<p>If you maintain a staging branch alongside main:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ~/staging staging</span></span></code></pre></div>
</div>
<p>Keep it around permanently. Push to staging by working in that directory. No branch switching ever.</p>
<h3 id="ci-simulation">CI simulation<a class="anchor" href="#ci-simulation" aria-label="Link to “CI simulation”">#</a></h3>
<p>Test your CI pipeline locally against a clean checkout:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add /tmp/ci-test HEAD
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/ci-test
</span></span><span class="line"><span class="cl">npm ci
</span></span><span class="line"><span class="cl">npm run lint
</span></span><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">npm run build</span></span></code></pre></div>
</div>
<p>This catches the classic &ldquo;works on my machine&rdquo; issues where you have uncommitted files or stale build artifacts that mask problems.</p>
<h2 id="managing-worktrees">Managing Worktrees<a class="anchor" href="#managing-worktrees" aria-label="Link to “Managing Worktrees”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List all worktrees</span>
</span></span><span class="line"><span class="cl">git worktree list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /home/user/project         abc1234 [main]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /home/user/project-hotfix  def5678 [hotfix/urgent-fix]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /tmp/ci-test               abc1234 (detached HEAD)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a worktree</span>
</span></span><span class="line"><span class="cl">git worktree remove /home/user/project-hotfix
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clean up stale worktree references</span>
</span></span><span class="line"><span class="cl">git worktree prune</span></span></code></pre></div>
</div>
<h2 id="gotchas">Gotchas<a class="anchor" href="#gotchas" aria-label="Link to “Gotchas”">#</a></h2>
<p>You cannot check out the same branch in two worktrees. Git prevents this because having the same branch in two places would make it ambiguous which working directory a commit applies to.</p>
<p>If you need to work on the same branch in two places, create a local tracking branch:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ../experiment experiment-copy
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ../experiment
</span></span><span class="line"><span class="cl">git checkout -b experiment-local origin/experiment</span></span></code></pre></div>
</div>
<p>The other thing to watch out for is node_modules. Each worktree needs its own <code>node_modules</code> because paths are different. The first <code>npm install</code> in a new worktree takes time. After that, it is fast because the npm cache is shared.</p>
<h2 id="vs-git-stash">vs. git stash<a class="anchor" href="#vs-git-stash" aria-label="Link to “vs. git stash”">#</a></h2>
<p>Stash is fine for quick interruptions. If someone asks you a question and you need to check something on another branch for thirty seconds, stash works.</p>
<p>But for anything that takes more than a minute, worktrees are better. They do not mess with your working directory, they let you have multiple contexts open in different editor windows, and they do not have the failure mode where you forget to pop the stash and lose track of what you were doing.</p>
<p>I keep a <code>~/worktrees/</code> directory for temporary ones and let them accumulate during the week. Friday afternoon I run <code>git worktree list</code> and clean up anything I am done with.</p>
]]></content:encoded></item><item><title>SSH Config Tricks I Use Every Day</title><link>https://articles.wleeaf.dev/posts/ssh-config-tricks/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/ssh-config-tricks/</guid><description>Stop typing full hostnames and passwords. Your SSH config file is more powerful than you think.</description><category>ssh</category><category>linux</category><category>productivity</category><content:encoded><![CDATA[<p>I SSH into servers maybe thirty times a day. Without a proper SSH config, that would mean typing out usernames, hostnames, ports, and key paths every single time. Life is too short for that.</p>
<h2 id="the-basics">The Basics<a class="anchor" href="#the-basics" aria-label="Link to “The Basics”">#</a></h2>
<p>The SSH config lives at <code>~/.ssh/config</code>. Here is a minimal example:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host myserver
  HostName 203.0.113.50
  User root
  IdentityFile ~/.ssh/id_ed25519</code></pre>
</div>
<p>Now <code>ssh myserver</code> is all you need. But it gets better.</p>
<h2 id="jump-hosts">Jump Hosts<a class="anchor" href="#jump-hosts" aria-label="Link to “Jump Hosts”">#</a></h2>
<p>If you need to reach a server through a bastion:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host bastion
  HostName bastion.example.com
  User admin

Host internal
  HostName 10.0.1.50
  User deploy
  ProxyJump bastion</code></pre>
</div>
<p><code>ssh internal</code> automatically tunnels through the bastion. No manual port forwarding, no two-step login.</p>
<h2 id="wildcard-hosts">Wildcard Hosts<a class="anchor" href="#wildcard-hosts" aria-label="Link to “Wildcard Hosts”">#</a></h2>
<p>This is the one most people miss:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *.prod
  User deploy
  IdentityFile ~/.ssh/prod_key
  StrictHostKeyChecking accept-new

Host web.prod
  HostName 10.0.1.10

Host api.prod
  HostName 10.0.1.11

Host db.prod
  HostName 10.0.1.12</code></pre>
</div>
<p>The wildcard block sets defaults for anything ending in <code>.prod</code>. Individual host blocks just add the hostname. You get consistent configuration without repetition.</p>
<h2 id="keep-connections-alive">Keep Connections Alive<a class="anchor" href="#keep-connections-alive" aria-label="Link to “Keep Connections Alive”">#</a></h2>
<p>Add this to your <code>Host *</code> block:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3
  AddKeysToAgent yes
  IdentitiesOnly yes</code></pre>
</div>
<p><code>ServerAliveInterval</code> sends a keepalive every 60 seconds. This stops firewalls and NATs from killing idle connections. <code>AddKeysToAgent</code> automatically loads keys into ssh-agent after first use.</p>
<h2 id="connection-multiplexing">Connection Multiplexing<a class="anchor" href="#connection-multiplexing" aria-label="Link to “Connection Multiplexing”">#</a></h2>
<p>This is the real speed trick:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  ControlMaster auto
  ControlPath ~/.ssh/sockets/%r@%h-%p
  ControlPersist 600</code></pre>
</div>
<p>The first SSH connection to a host creates a socket. Subsequent connections to the same host reuse it. No new TCP handshake, no new key exchange. The second <code>ssh myserver</code> connects instantly.</p>
<p><code>ControlPersist 600</code> keeps the socket alive for 10 minutes after you disconnect. So if you SSH in, run a command, disconnect, and SSH back in within 10 minutes, the second connection is instant.</p>
<p>Create the socket directory first:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.ssh/sockets</span></span></code></pre></div>
</div>
<h2 id="port-forwarding-shortcuts">Port Forwarding Shortcuts<a class="anchor" href="#port-forwarding-shortcuts" aria-label="Link to “Port Forwarding Shortcuts”">#</a></h2>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host db-tunnel
  HostName myserver.com
  User admin
  LocalForward 5432 localhost:5432
  RequestTTY no
  ExitOnForwardFailure yes</code></pre>
</div>
<p>Now <code>ssh db-tunnel</code> opens a tunnel to the remote Postgres. No remembering the <code>-L</code> syntax. <code>ExitOnForwardFailure</code> means if the port is already in use, SSH exits instead of connecting without the tunnel.</p>
<h2 id="sshfp-for-host-verification">SSHFP for Host Verification<a class="anchor" href="#sshfp-for-host-verification" aria-label="Link to “SSHFP for Host Verification”">#</a></h2>
<p>Tired of the &ldquo;Are you sure you want to continue connecting?&rdquo; messages? If you control the DNS, add SSHFP records:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -r myserver.com</span></span></code></pre></div>
</div>
<p>This outputs DNS records you can add to your zone. Then set:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>Host *
  VerifyHostKeyDNS yes</code></pre>
</div>
<p>SSH verifies the host key against DNS automatically. No more blindly accepting fingerprints.</p>
<p>The SSH config file is probably the highest-leverage dotfile you can invest time in. Five minutes of setup saves thousands of keystrokes.</p>
]]></content:encoded></item></channel></rss>