<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Tutorial on Cole World</title><link>https://blog.colemei.com/tags/tutorial/</link><description>Recent content in Tutorial on Cole World</description><generator>Hugo -- 0.150.1</generator><language>en</language><copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright><lastBuildDate>Mon, 06 Apr 2026 12:00:00 +1000</lastBuildDate><atom:link href="https://blog.colemei.com/tags/tutorial/index.xml" rel="self" type="application/rss+xml"/><item><title>One Machine, Two OSes: A Seamless Remote Dev Pipeline</title><link>https://blog.colemei.com/posts/one-machine-two-oses-a-seamless-remote-dev-pipeline/</link><pubDate>Mon, 06 Apr 2026 12:00:00 +1000</pubDate><author>Cole Mei</author><guid>https://blog.colemei.com/posts/one-machine-two-oses-a-seamless-remote-dev-pipeline/</guid><description>How I wired macOS and WSL2 into one seamless dev environment with Tailscale and 1Password SSH agent.</description><enclosure url="https://blog.colemei.com/1067-1920x1080_2672996642849076272.jpg" length="" type="image/jpeg"/><media:thumbnail url="https://blog.colemei.com/1067-1920x1080_2672996642849076272.jpg" width="1920" height="1080"/><media:content url="https://blog.colemei.com/1067-1920x1080_2672996642849076272.jpg" medium="image" type="image/jpeg" width="1920" height="1080"><media:title type="html">/1067-1920x1080_2672996642849076272.jpg</media:title></media:content><content type="text/html" mode="escaped"><![CDATA[<h2 id="the-problem-two-machines-one-workflow">The problem: two machines, one workflow<a href="#the-problem-two-machines-one-workflow" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>If you work in research or anything that involves heavy compute, you&rsquo;ve probably landed in this situation: your daily driver is a MacBook (great keyboard, great display, great battery), but your workloads need a beefy desktop with a GPU, more RAM, or specific OS-level tooling.</p>
<p>My setup is a MacBook Pro for everyday development and a Windows desktop running WSL2 with Ubuntu. Until recently I did most things on the Mac — including running experiments in Docker. That changed when I started working on a security research benchmark: one of the dynamic analysis tools had a compatibility issue with Docker on macOS and simply wouldn&rsquo;t run. I had to move the experiment execution stage to WSL2. Once I did, I also realized the desktop&rsquo;s extra CPU and RAM cut experiment runtimes significantly, so it made sense to keep it that way.</p>
<p>I already use Tailscale across all my devices and keep all my SSH keys in 1Password — so the question was less &ldquo;what tools do I need&rdquo; and more &ldquo;how do I wire what I already have into a pipeline where I never have to touch the Windows desktop.&rdquo; Everything should happen through the terminal on the Mac.</p>
<p>The catch? WSL2 gets a new internal IP every time Windows reboots. And without a bit of plumbing, you end up manually hunting for IPs and re-running commands every few weeks.</p>
<p>Here&rsquo;s how I wired it all together.</p>
<h2 id="architecture-overview">Architecture overview<a href="#architecture-overview" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Before diving into the details, here&rsquo;s how all the pieces fit together:</p>
<figure class="svg-diagram ">
  
<svg viewBox="0 0 690 420" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
    </marker>
  </defs>

  <!-- Tailscale mesh cloud -->
  <rect x="165" y="165" width="360" height="90" rx="45" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 4" opacity="0.5"/>
  <text x="345" y="195" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0F6E56" font-weight="500" opacity="0.7">Tailscale private mesh (100.x.x.x)</text>

  <!-- macOS box -->
  <g>
    <rect x="40" y="30" width="190" height="100" rx="10" fill="#E1F5EE" stroke="#1D9E75" stroke-width="0.8"/>
    <text x="135" y="62" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#085041" font-weight="500">macOS</text>
    <text x="135" y="82" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0F6E56">develop · edit · analyze</text>
    <text x="135" y="100" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0F6E56">1Password agent</text>
  </g>

  <!-- Windows/WSL2 box -->
  <g>
    <rect x="460" y="30" width="190" height="100" rx="10" fill="#E6F1FB" stroke="#378ADD" stroke-width="0.8"/>
    <text x="555" y="55" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#0C447C" font-weight="500">Windows desktop</text>
    <rect x="475" y="68" width="160" height="50" rx="6" fill="#B5D4F4" stroke="#378ADD" stroke-width="0.5"/>
    <text x="555" y="88" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#042C53" font-weight="500">WSL2 (Ubuntu)</text>
    <text x="555" y="105" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#185FA5">Docker · GPU · compute</text>
  </g>

  <!-- SSH arrow Mac -> WSL2 -->
  <path d="M230 75 Q345 60 460 75" fill="none" stroke="#1D9E75" stroke-width="1.5" marker-end="url(#arr)"/>
  <text x="345" y="52" text-anchor="middle" font-family="monospace" font-size="10" fill="#0F6E56">ssh wsl</text>

  <!-- rsync arrow WSL2 -> Mac -->
  <path d="M460 110 Q345 150 230 110" fill="none" stroke="#378ADD" stroke-width="1.5" marker-end="url(#arr)"/>
  <text x="345" y="145" text-anchor="middle" font-family="monospace" font-size="10" fill="#185FA5">rsync results</text>

  <!-- Port forwarding detail -->
  <g>
    <rect x="410" y="280" width="240" height="104" rx="8" fill="#FAEEDA" stroke="#EF9F27" stroke-width="0.6"/>
    <text x="530" y="305" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#854F0B" font-weight="500">Windows port forward</text>
    <text x="530" y="324" text-anchor="middle" font-family="monospace" font-size="10" fill="#854F0B">:2222 → WSL2 :22</text>
    <text x="530" y="346" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#854F0B">Auto-runs on startup via</text>
    <text x="530" y="362" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#854F0B">Task Scheduler + PowerShell</text>
  </g>

  <!-- 1Password detail -->
  <g>
    <rect x="40" y="280" width="240" height="104" rx="8" fill="#EEEDFE" stroke="#7F77DD" stroke-width="0.6"/>
    <text x="160" y="305" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#3C3489" font-weight="500">1Password SSH agent</text>
    <text x="160" y="326" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#534AB7">Keys never leave 1Password</text>
    <text x="160" y="346" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#534AB7">Biometric unlock on macOS</text>
    <text x="160" y="366" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#534AB7">Agent forwarding to WSL2</text>
  </g>

  <!-- Connectors to detail boxes -->
  <line x1="135" y1="130" x2="135" y2="278" stroke="#7F77DD" stroke-width="0.8" stroke-dasharray="4 3" marker-end="url(#arr)"/>
  <line x1="555" y1="130" x2="555" y2="278" stroke="#EF9F27" stroke-width="0.8" stroke-dasharray="4 3" marker-end="url(#arr)"/>

  <!-- tmux label -->
  <rect x="310" y="380" width="70" height="24" rx="12" fill="#F1EFE8" stroke="#B4B2A9" stroke-width="0.5"/>
  <text x="345" y="396" text-anchor="middle" font-family="monospace" font-size="10" fill="#5F5E5A">tmux</text>
  <text x="345" y="416" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#888780">persistent sessions</text>
</svg>

  <figcaption>Figure 1 — High-level architecture: macOS develops, WSL2 executes, Tailscale connects the Mac to Windows, port forwarding reaches WSL2.</figcaption>
</figure>

<p>Each piece solves one specific annoyance: Tailscale gives stable IPs; 1Password removes key management; The port-forwarding script handles the WSL2 dynamic IP; tmux keeps experiments alive when SSH drops.</p>
<p>Next, let&rsquo;s walk through each.</p>
<hr>
<h2 id="exposing-wsl2-after-every-reboot">Exposing WSL2 after every reboot<a href="#exposing-wsl2-after-every-reboot" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>WSL2 runs inside a lightweight VM, which means it gets a fresh internal IP address (in the <code>172.x.x.x</code> range) every time Windows reboots. This breaks any hardcoded SSH config pointing at WSL2.</p>
<p>The fix is a small PowerShell script that runs on Windows startup. It discovers the current WSL2 IP, tears down any stale port forwarding rules, and sets up a fresh forward from Windows port <code>2222</code> to WSL2 port <code>22</code>.</p>
<h3 id="the-powershell-script">The PowerShell script<a href="#the-powershell-script" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># wsl2-port-forward.ps1</span>
</span></span><span class="line"><span class="cl"><span class="c"># Get the current WSL2 IP</span>
</span></span><span class="line"><span class="cl"><span class="nv">$wslIp</span> <span class="p">=</span> <span class="p">(</span><span class="n">wsl</span> <span class="n">hostname</span> <span class="n">-I</span><span class="p">).</span><span class="py">Trim</span><span class="p">().</span><span class="py">Split</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)[</span><span class="mf">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Remove stale rules</span>
</span></span><span class="line"><span class="cl"><span class="n">netsh</span> <span class="n">interface</span> <span class="n">portproxy</span> <span class="n">reset</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Forward port 2222 on all Windows interfaces to WSL2:22</span>
</span></span><span class="line"><span class="cl"><span class="n">netsh</span> <span class="n">interface</span> <span class="n">portproxy</span> <span class="n">add</span> <span class="n">v4tov4</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">listenport</span><span class="p">=</span><span class="mf">2222</span> <span class="n">listenaddress</span><span class="p">=</span><span class="mf">0.0</span><span class="p">.</span><span class="py">0</span><span class="p">.</span><span class="py">0</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">  <span class="n">connectport</span><span class="p">=</span><span class="mf">22</span> <span class="n">connectaddress</span><span class="p">=</span><span class="nv">$wslIp</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Allow through Windows Firewall (run once)</span>
</span></span><span class="line"><span class="cl"><span class="c"># New-NetFirewallRule -DisplayName &#34;WSL2 SSH&#34; `</span>
</span></span><span class="line"><span class="cl"><span class="c">#   -Direction Inbound -LocalPort 2222 -Protocol TCP -Action Allow</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Write-Host</span> <span class="s2">&#34;Forwarding 0.0.0.0:2222 -&gt; </span><span class="nv">${wslIp}</span><span class="s2">:22&#34;</span>
</span></span></code></pre></div><h3 id="automate-it-with-task-scheduler">Automate it with Task Scheduler<a href="#automate-it-with-task-scheduler" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Create a scheduled task that triggers &ldquo;At log on&rdquo; (or &ldquo;At startup&rdquo;) running this script with elevated privileges. After that, every reboot is handled automatically — no manual IP hunting.</p>
<blockquote>
<p><strong>Why port 2222?</strong>
Using a non-standard port avoids conflicts if Windows itself is running an SSH server on port 22. It also makes it immediately clear in your SSH config which connections are going to WSL2 versus a native host.</p></blockquote>
<hr>
<h2 id="tailscale-stable-ips-without-thinking">Tailscale: stable IPs without thinking<a href="#tailscale-stable-ips-without-thinking" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>I&rsquo;ve been running <a href="https://tailscale.com">Tailscale</a> on all my devices for a while now, it&rsquo;s one of those tools that disappears into the background once set up. It creates a WireGuard-based mesh VPN where each device gets a stable <code>100.x.x.x</code> address that works from anywhere. No port forwarding on your router, no dynamic DNS.</p>
<p>For this pipeline, Tailscale only needs to be on two things:</p>
<ol>
<li><strong>macOS</strong> — via the Mac App Store or <code>brew install tailscale</code></li>
<li><strong>Windows</strong> — via the Tailscale Windows installer</li>
</ol>
<p>Since we already have port forwarding from Windows port <code>2222</code> to WSL2 port <code>22</code>, there&rsquo;s no need to install Tailscale inside WSL2. The Mac connects to the Windows host&rsquo;s Tailscale IP, and the port forward takes care of the rest.</p>
<h3 id="ssh-config-on-the-mac">SSH config on the Mac<a href="#ssh-config-on-the-mac" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>With Tailscale and 1Password SSH agent both running, the Mac-side <code>~/.ssh/config</code> looks like this:</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config"># ~/.ssh/config
# Use 1Password SSH agent for all hosts
Host *
  IdentityAgent ~/.1password/agent.sock
  Include ~/.ssh/1Password/config
  IdentitiesOnly yes

# WSL2 via Windows host&#39;s Tailscale IP
Host wsl
  HostName 100.x.x.x            # Windows host&#39;s Tailscale IP
  User your-username
  Port 2222
  IdentityFile ~/.ssh/keys/your-key.pub
</code></pre><p>Now <code>ssh wsl</code> from anywhere lands you in your Ubuntu session — the IP never changes, so this config survives reboots indefinitely.</p>
<hr>
<h2 id="1password-ssh-agent-keys-without-key-files">1Password SSH agent: keys without key files<a href="#1password-ssh-agent-keys-without-key-files" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>I switched to 1Password&rsquo;s SSH agent a while back and haven&rsquo;t looked at a raw key file since. All my SSH keys live in my 1Password vault — encrypted, synced, and never written to disk. The 1Password app on macOS exposes an agent socket that any SSH client can use, with biometric (Touch ID) approval for each connection.</p>
<h3 id="how-it-fits-into-this-pipeline">How it fits into this pipeline<a href="#how-it-fits-into-this-pipeline" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<ol>
<li><strong>Keys live in 1Password.</strong> Generate or import your SSH keys directly in the 1Password app. No <code>~/.ssh/id_*</code> files on disk.</li>
<li><strong>macOS uses the 1Password agent.</strong> The <code>IdentityAgent</code> and <code>IdentityFile</code> lines in the SSH config (shown above) tell SSH to ask 1Password for the key. When you <code>ssh wsl</code>, Touch ID fires, and you&rsquo;re in.</li>
<li><strong>Agent forwarding passes the key to WSL2.</strong> With agent forwarding, your 1Password-managed key is available inside the WSL2 session too. You can <code>git pull</code> from private repos or <code>rsync</code> back to the Mac — all authenticated through the forwarded agent.</li>
</ol>
<p>In 1Password settings, make sure the SSH agent is enabled under <strong>Developer → SSH Agent</strong>. The agent will serve any key in your vault that has the &ldquo;Use for SSH&rdquo; toggle turned on.</p>
<blockquote>
<p><strong>Security benefit.</strong>
Your private keys never exist as files on disk — on <em>any</em> machine. They&rsquo;re encrypted in 1Password&rsquo;s vault, decrypted only in memory when you approve a request. If your WSL2 instance or Mac is compromised, there&rsquo;s no <code>~/.ssh/</code> directory to exfiltrate.</p></blockquote>
<h3 id="the-windows-side-bridging-1password-into-wsl2">The Windows side: bridging 1Password into WSL2<a href="#the-windows-side-bridging-1password-into-wsl2" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>1Password runs on Windows too, but WSL2 is a separate Linux VM — the agent&rsquo;s named pipe doesn&rsquo;t exist inside WSL2. The bridge is <a href="https://github.com/jstarks/npiperelay"><code>npiperelay</code></a> combined with <code>socat</code>: a small relay that exposes the Windows 1Password agent as a Unix socket inside WSL2. When you <code>git push</code> from a local WSL2 terminal, the request travels through the relay to 1Password, the GUI pops up, you approve with Windows Hello, and the key is served.</p>
<p>This works great — until you SSH into WSL2 from the Mac. There&rsquo;s no desktop session, no GUI, no way for 1Password to show its popup. The bridge just hangs.</p>
<p>The fix is a conditional block in <code>~/.zshrc</code> that detects how you entered WSL2:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zsh" data-lang="zsh"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$SSH_CONNECTION</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="c1"># Remote session — use a dedicated headless key</span>
</span></span><span class="line"><span class="cl">    ln -sf ~/.ssh/config.headless ~/.ssh/config
</span></span><span class="line"><span class="cl">    <span class="nb">eval</span> <span class="k">$(</span>keychain --eval --quiet ~/.ssh/id_ed25519_headless 2&gt;/dev/null<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Local desktop — bridge to 1Password agent</span>
</span></span><span class="line"><span class="cl">    ln -sf ~/.ssh/config.local ~/.ssh/config
</span></span><span class="line"><span class="cl">    <span class="nb">export</span> <span class="nv">SSH_AUTH_SOCK</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.ssh/agent.sock&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="o">[[</span> <span class="nv">$-</span> <span class="o">==</span> *i* <span class="o">]]</span> <span class="o">&amp;&amp;</span> setsid socat <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>        UNIX-LISTEN:<span class="s2">&#34;</span><span class="nv">$SSH_AUTH_SOCK</span><span class="s2">&#34;</span>,mode<span class="o">=</span>600,unlink-early,fork <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>        EXEC:<span class="s2">&#34;/mnt/c/Windows/System32/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent&#34;</span>,nofork <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>        &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>It also symlinks the right SSH config: the local version lets 1Password handle key selection; the headless version pins a dedicated key with <code>IdentitiesOnly yes</code> for GitHub and the Mac. The headless key (<code>id_ed25519_headless</code>) is the one key that lives on disk — it&rsquo;s added to <code>authorized_keys</code> on the Mac and to GitHub, and only gets used when the GUI isn&rsquo;t available.</p>
<p>The result: at the desktop you get the full biometric popup; from the Mac you get seamless headless auth.</p>
<hr>
<h2 id="persistent-sessions-with-tmux">Persistent sessions with tmux<a href="#persistent-sessions-with-tmux" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Long-running experiments will outlive your SSH connection. Wi-Fi drops, laptops sleep, and VPN tunnels time out. Without a persistent session, your process dies when the connection does.</p>
<p><code>tmux</code> is the standard solution. Start a named tmux session on WSL2, run your experiment inside it, and detach freely. Reconnect whenever you want.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># On WSL2: start a named session</span>
</span></span><span class="line"><span class="cl">tmux new -s experiment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run your workload inside the session</span>
</span></span><span class="line"><span class="cl">python run_experiment.py --config my_config.toml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Detach: Ctrl+B then D</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Reconnect later:</span>
</span></span><span class="line"><span class="cl">tmux attach -t experiment
</span></span></code></pre></div><p>I name sessions after the experiment or task, so <code>tmux ls</code> gives me a quick dashboard of what&rsquo;s running.</p>
<h3 id="what-this-looks-like-in-practice">What this looks like in practice<a href="#what-this-looks-like-in-practice" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>In my recent research project, my typical flow is: develop and iterate on the Mac, then SSH into WSL2 to kick off the real run. Here&rsquo;s a concrete example — starting from the Mac terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. SSH into WSL2</span>
</span></span><span class="line"><span class="cl">ssh wsl
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Pull the latest code</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/projects/my-project
</span></span><span class="line"><span class="cl">git pull
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Start a named tmux session and run the experiment</span>
</span></span><span class="line"><span class="cl">tmux new -s baseline-run
</span></span><span class="line"><span class="cl">python src/run_experiment.py --config configs/baseline.toml
</span></span></code></pre></div><p>Detach with <code>Ctrl-b d</code>, close the laptop, go get coffee or sleep. Later:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Reconnect from anywhere</span>
</span></span><span class="line"><span class="cl">ssh wsl
</span></span><span class="line"><span class="cl">tmux attach -t baseline-run
</span></span></code></pre></div><p>The Mac is the control plane — I write code, tweak configs, run quick smoke tests locally. The moment I need real compute (GPU, large dataset, Docker), I <code>ssh wsl</code> and run it there. The Windows desktop itself never needs to be touched.</p>
<hr>
<h2 id="syncing-results-back-to-macos">Syncing results back to macOS<a href="#syncing-results-back-to-macos" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Once an experiment finishes on WSL2, I want the results on my Mac for analysis. A simple <code>rsync</code> handles this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># From WSL2, sync a specific run back to the Mac</span>
</span></span><span class="line"><span class="cl">rsync -avz --progress <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  ~/projects/my-project/runs/baseline/2026-04-05_14-30/ <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  mac:~/projects/my-project/runs/baseline/2026-04-05_14-30/
</span></span></code></pre></div><p>The <code>mac</code> host alias works because the Mac is also on the Tailnet with a stable IP, and WSL2&rsquo;s agent-forwarded SSH key authenticates the connection. No passwords, no key files.</p>
<p>For repeated use, I wrap this in a small shell script that takes an experiment name and a run timestamp, resolves the paths, and handles edge cases like partial transfers or syncing evaluation results alongside the raw run output. Something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># A wrapper script that syncs run + eval directories</span>
</span></span><span class="line"><span class="cl">bash scripts/sync_run_to_mac.sh baseline 2026-04-05_14-30
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Preview what would be synced without actually transferring</span>
</span></span><span class="line"><span class="cl">bash scripts/sync_run_to_mac.sh --dry-run baseline 2026-04-05_14-30
</span></span></code></pre></div><blockquote>
<p><strong>Keep large runs out of git.</strong>
Experiment outputs can be gigabytes of logs, checkpoints, and evaluation artifacts. Add your runs directory to <code>.gitignore</code> and treat rsync as the transport layer. Commit only code, configs, and lightweight analysis.</p></blockquote>
<hr>
<h2 id="bonus-quality-of-life-extras">Bonus: quality-of-life extras<a href="#bonus-quality-of-life-extras" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<h3 id="auto-start-ssh-server-in-wsl2">Auto-start SSH server in WSL2<a href="#auto-start-ssh-server-in-wsl2" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>WSL2 doesn&rsquo;t start <code>sshd</code> automatically. Add this to your WSL2&rsquo;s <code>/etc/wsl.conf</code> (WSL2 in Windows 11 supports <code>[boot]</code> commands):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/wsl.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[boot]</span>
</span></span><span class="line"><span class="cl"><span class="na">command</span> <span class="o">=</span> <span class="s">service ssh start</span>
</span></span></code></pre></div><p>Now <code>sshd</code> starts every time WSL2 boots, no manual intervention needed.</p>
<h3 id="git-commit-signing-with-1password">Git commit signing with 1Password<a href="#git-commit-signing-with-1password" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Since your SSH keys are already in 1Password, you can use the same setup for signing git commits. 1Password can automatically configure your local <code>~/.gitconfig</code> with the right signing key. Every commit is signed, verified on GitHub, and the key never touches disk.</p>
<h3 id="monitoring-from-the-mac">Monitoring from the Mac<a href="#monitoring-from-the-mac" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>For long-running jobs, I sometimes want to glance at GPU usage or system load without SSHing in. A lightweight solution is to run a small status script on WSL2 that periodically writes a JSON summary, then pull it with a cron job on the Mac.</p>
<hr>
<h2 id="wrapping-up">Wrapping up<a href="#wrapping-up" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>None of the individual pieces here are novel — Tailscale, 1Password, tmux, rsync are all tools I was already using. The value is in wiring them together into a pipeline where the daily workflow is just:</p>
<ol>
<li>Write code and configs on the Mac</li>
<li><code>ssh wsl</code> — land in Ubuntu on the desktop</li>
<li><code>git pull</code>, start a tmux session, run the experiment</li>
<li>Detach, close the laptop, come back later</li>
<li>Sync results back, analyze locally</li>
</ol>
<p>The Mac is always the control plane. The Windows box never needs its monitor turned on.</p>
]]></content></item><item><title>A Clean, Low-Leakage API Key Workflow for Local Dev</title><link>https://blog.colemei.com/posts/a-clean-low-leakage-api-key-workflow-for-local-dev/</link><pubDate>Tue, 03 Feb 2026 17:35:19 +0800</pubDate><author>Cole Mei</author><guid>https://blog.colemei.com/posts/a-clean-low-leakage-api-key-workflow-for-local-dev/</guid><description>Integrating 1Password Environments into a practical engineering workflow</description><enclosure url="https://blog.colemei.com/37-1920x1080_7055121760532388279.jpg" length="" type="image/jpeg"/><media:thumbnail url="https://blog.colemei.com/37-1920x1080_7055121760532388279.jpg" width="1920" height="1080"/><media:content url="https://blog.colemei.com/37-1920x1080_7055121760532388279.jpg" medium="image" type="image/jpeg" width="1920" height="1080"><media:title type="html">/37-1920x1080_7055121760532388279.jpg</media:title></media:content><content type="text/html" mode="escaped"><![CDATA[<h2 id="introduction">Introduction<a href="#introduction" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>In 2026, local development commonly involves multiple API providers and multiple identities. It is normal to switch between work, personal, and research contexts within the same machine and even the same terminal session.</p>
<p>Despite this, API key management during local development is still largely informal. <code>.env</code> files are copied around, shell environments silently accumulate secrets, and key rotation is postponed because it is operationally painful.</p>
<p>The core problem is not &ldquo;where to store API keys&rdquo;. Most developers already know the answer is &ldquo;a password manager or secret manager&rdquo;. The real problems are subtler.</p>
<p>These issues create friction and, more importantly, increase the risk of accidental leaks. The idea of separating config from code traces back to <a href="https://12factor.net/config">The Twelve-Factor App</a>, which popularized environment variables over config files. That was a meaningful step forward — but it led to <code>.env</code> files storing credentials in plaintext, protected by nothing more than a <code>.gitignore</code> entry.</p>
<p>What we need is not just secure storage, but <strong>explicit lifecycle control</strong>.</p>
<p>This post documents a workflow I use daily to manage API keys locally in a way that is explicit, low-leakage, and scalable. The focus is not on tools themselves, but on <strong>how to integrate them into a coherent engineering design</strong>.</p>
<hr>
<h2 id="design-principles">Design principles<a href="#design-principles" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Before choosing tools, I defined a few constraints:</p>
<ul>
<li><strong>Secrets should have a single source of truth</strong>. No duplication, no secondary copies.</li>
<li><strong>Secrets should not live longer than necessary</strong>. A key used for one command should not survive in the shell afterwards.</li>
<li><strong>Projects should be isolated by default</strong>. Entering a directory should not implicitly grant access to unrelated credentials.</li>
<li><strong>The workflow must remain ergonomic</strong>. Security that disrupts daily work will eventually be bypassed.</li>
</ul>
<p>These principles guided all design decisions below.</p>
<hr>
<h2 id="high-level-architecture">High-level architecture<a href="#high-level-architecture" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>At a high level, the system separates responsibility into three layers:</p>
<figure class="svg-diagram ">
  
<svg viewBox="0 0 700 520" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
    </marker>
    <filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
      <feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.06"/>
    </filter>
  </defs>
  <!-- Title -->
  <text x="350" y="28" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="11" fill="#6B6A64" font-weight="500" letter-spacing="0.08em" text-transform="uppercase">SECRET LIFECYCLE ARCHITECTURE</text>
  <!-- Layer 1: Source of Truth -->
  <g filter="url(#shadow)">
    <rect x="170" y="50" width="360" height="70" rx="10" fill="#EEEDFE" stroke="#7F77DD" stroke-width="1"/>
    <text x="350" y="78" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="14" fill="#3C3489" font-weight="600">1Password Environments</text>
    <text x="350" y="100" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="11" fill="#534AB7">Single source of truth · Encrypted · Synced</text>
  </g>
  <!-- Arrow down -->
  <line x1="350" y1="120" x2="350" y2="155" stroke="#7F77DD" stroke-width="1.5" marker-end="url(#arr)"/>
  <!-- Layer 2: Distribution -->
  <g filter="url(#shadow)">
    <rect x="80" y="160" width="540" height="70" rx="10" fill="#E1F5EE" stroke="#1D9E75" stroke-width="1"/>
    <text x="350" y="188" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="14" fill="#085041" font-weight="600">Distribution Layer</text>
    <text x="350" y="210" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="11" fill="#0F6E56">Local .env (FIFO) · op run · SDK (Go / Python / JS)</text>
  </g>
  <!-- Three arrows fanning out -->
  <line x1="210" y1="230" x2="150" y2="280" stroke="#1D9E75" stroke-width="1.2" marker-end="url(#arr)"/>
  <line x1="350" y1="230" x2="350" y2="280" stroke="#1D9E75" stroke-width="1.2" marker-end="url(#arr)"/>
  <line x1="490" y1="230" x2="550" y2="280" stroke="#1D9E75" stroke-width="1.2" marker-end="url(#arr)"/>
  <!-- Layer 3: Three consumption patterns -->
  <!-- Pattern 1: Project-scoped -->
  <g filter="url(#shadow)">
    <rect x="40" y="285" width="210" height="100" rx="8" fill="#fff" stroke="#1D6B50" stroke-width="0.8"/>
    <text x="145" y="312" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="13" fill="#1D6B50" font-weight="600">Project-Scoped</text>
    <text x="145" y="334" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="10" fill="#1A1A18">.env FIFO + direnv</text>
    <text x="145" y="354" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">Secrets live while</text>
    <text x="145" y="370" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">you're in the directory</text>
  </g>
  <!-- Pattern 2: Command-scoped -->
  <g filter="url(#shadow)">
    <rect x="265" y="285" width="170" height="100" rx="8" fill="#fff" stroke="#378ADD" stroke-width="0.8"/>
    <text x="350" y="312" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="13" fill="#185FA5" font-weight="600">Command-Scoped</text>
    <text x="350" y="334" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="10" fill="#1A1A18">op run --environment</text>
    <text x="350" y="354" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">Secrets live for</text>
    <text x="350" y="370" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">one command only</text>
  </g>
  <!-- Pattern 3: Programmatic -->
  <g filter="url(#shadow)">
    <rect x="450" y="285" width="210" height="100" rx="8" fill="#fff" stroke="#EF9F27" stroke-width="0.8"/>
    <text x="555" y="312" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="13" fill="#854F0B" font-weight="600">Programmatic</text>
    <text x="555" y="334" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="10" fill="#1A1A18">SDK / op env read</text>
    <text x="555" y="354" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">Secrets fetched</text>
    <text x="555" y="370" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">on demand in code</text>
  </g>
  <!-- Bottom: lifecycle bar -->
  <rect x="40" y="415" width="620" height="44" rx="8" fill="#F0EFE9" stroke="#D3D1C7" stroke-width="0.6"/>
  <text x="350" y="434" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="11" fill="#1A1A18" font-weight="500">Lifecycle Guarantees</text>
  <text x="350" y="450" text-anchor="middle" font-family="DM Sans, sans-serif" font-size="10" fill="#6B6A64">Never on disk as plaintext · Never in Git · Never in shell history · Scoped lifetime</text>
  <!-- Arrows to lifecycle bar -->
  <line x1="145" y1="385" x2="145" y2="413" stroke="#D3D1C7" stroke-width="0.8" stroke-dasharray="4 3"/>
  <line x1="350" y1="385" x2="350" y2="413" stroke="#D3D1C7" stroke-width="0.8" stroke-dasharray="4 3"/>
  <line x1="555" y1="385" x2="555" y2="413" stroke="#D3D1C7" stroke-width="0.8" stroke-dasharray="4 3"/>
  <!-- Legend -->
  <g>
    <rect x="40" y="480" width="10" height="10" rx="2" fill="#EEEDFE" stroke="#7F77DD" stroke-width="0.5"/>
    <text x="56" y="489" font-family="DM Sans, sans-serif" font-size="9.5" fill="#6B6A64">Storage</text>
    <rect x="120" y="480" width="10" height="10" rx="2" fill="#E1F5EE" stroke="#1D9E75" stroke-width="0.5"/>
    <text x="136" y="489" font-family="DM Sans, sans-serif" font-size="9.5" fill="#6B6A64">Distribution</text>
    <rect x="220" y="480" width="10" height="10" rx="2" fill="#fff" stroke="#1D6B50" stroke-width="0.5"/>
    <text x="236" y="489" font-family="DM Sans, sans-serif" font-size="9.5" fill="#6B6A64">Consumption</text>
  </g>
</svg>

  <figcaption>Figure 1 — Three-layer architecture: 1Password owns the secrets, the distribution layer mediates access, and each consumption pattern enforces a different lifetime scope.</figcaption>
</figure>

<p>Long story short:</p>
<p>1Password owns the secrets. The distribution layer provides multiple mechanisms to surface them — FIFO-mounted <code>.env</code> files, the CLI, or native SDKs. Different consumption patterns then control <em>when</em> and <em>how long</em> those secrets are available.</p>
<p>There are three supported usage patterns. Keeping them distinct is intentional.</p>
<hr>
<h2 id="pattern-1-project-scoped-mount-primary-workflow">Pattern 1: Project-scoped mount (primary workflow)<a href="#pattern-1-project-scoped-mount-primary-workflow" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>This pattern answers the question: <em>&ldquo;Which credentials does this project use?&rdquo;</em></p>
<p>A 1Password Environment (e.g. <code>Dev-Work</code>) is mounted directly to the project root as <code>./.env</code>. Under the hood, 1Password creates a UNIX named pipe (FIFO) — not a regular file. Your existing dotenv libraries read it transparently, but no plaintext secrets ever land on disk. The mounted file remains available as long as 1Password is running and locks automatically when 1Password locks.</p>
<blockquote>
<p><strong>How the FIFO works.</strong> When your app reads the <code>.env</code> path, 1Password intercepts the read via the named pipe and streams the secret values directly to the process through a UNIX pipe. The data passes through memory only. Because the mount is not a regular file, it won&rsquo;t be staged, committed, or pushed by Git — even without a <code>.gitignore</code> entry.</p></blockquote>
<h3 id="step-1-create-and-mount-an-environment">Step 1: Create and mount an Environment<a href="#step-1-create-and-mount-an-environment" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>In the 1Password desktop app, navigate to <strong>Developer → View Environments</strong>. Create environments such as <code>Dev-Work</code> and <code>Dev-Personal</code>, keeping variable names identical across environments.</p>
<p>Under the <strong>Destinations</strong> tab, select <strong>Configure destination</strong> for &ldquo;Local .env file&rdquo;, then choose the project root as the mount path:</p>
<pre tabindex="0"><code>project/.env
</code></pre><p>You can import an existing <code>.env</code> file directly, or add key-value pairs manually. 1Password will begin serving the FIFO immediately.</p>
<blockquote>
<p><strong>Git housekeeping.</strong> If you already have a plaintext <code>.env</code> tracked by Git at that path, delete it and commit the removal <em>before</em> mounting. Otherwise <code>git status</code> may report the FIFO as a changed file. The contents can never actually be staged, so your secrets remain safe — but the noise is annoying.</p></blockquote>
<h3 id="step-2-install-and-enable-direnv">Step 2: Install and enable direnv<a href="#step-2-install-and-enable-direnv" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p><code>direnv</code> is optional but strongly recommended. It manages shell-level visibility: enter a directory and variables appear; leave and they disappear.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install direnv
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;eval &#34;$(direnv hook zsh)&#34;&#39;</span> &gt;&gt; ~/.zshrc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.zshrc
</span></span></code></pre></div><h3 id="step-3-define-a-global-loader-for-env">Step 3: Define a global loader for <code>.env</code><a href="#step-3-define-a-global-loader-for-env" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Create a helper that tells <code>direnv</code> how to load project secrets:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.config/direnv/lib
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt; ~/.config/direnv/lib/1password_env.sh <span class="s">&lt;&lt;&#39;SH&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">use_1password_env() {
</span></span></span><span class="line"><span class="cl"><span class="s">  # Ensure dotenv is available
</span></span></span><span class="line"><span class="cl"><span class="s">  type -t dotenv &gt;/dev/null 2&gt;&amp;1 || {
</span></span></span><span class="line"><span class="cl"><span class="s">    log_error &#34;direnv stdlib &#39;dotenv&#39; not available&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">    return 1
</span></span></span><span class="line"><span class="cl"><span class="s">  }
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">  # Load project .env (FIFO or regular file)
</span></span></span><span class="line"><span class="cl"><span class="s">  if [ -e .env ]; then
</span></span></span><span class="line"><span class="cl"><span class="s">    dotenv .env
</span></span></span><span class="line"><span class="cl"><span class="s">  else
</span></span></span><span class="line"><span class="cl"><span class="s">    log_status &#34;no .env found in this directory&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">  fi
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">  # Optional local overrides (non-secret config)
</span></span></span><span class="line"><span class="cl"><span class="s">  type -t dotenv_if_exists &gt;/dev/null 2&gt;&amp;1 &amp;&amp; dotenv_if_exists .env.local
</span></span></span><span class="line"><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="cl"><span class="s">SH</span>
</span></span></code></pre></div><p>This helper never stores secrets. It only consumes <code>.env</code> if it exists.</p>
<h3 id="step-4-project-root-envrc">Step 4: Project root <code>.envrc</code><a href="#step-4-project-root-envrc" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>In the project root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .envrc</span>
</span></span><span class="line"><span class="cl">use 1password_env
</span></span></code></pre></div><p>Then allow it once:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">direnv allow
</span></span></code></pre></div><p>From now on, entering the project directory automatically injects the secrets; leaving it removes them.</p>
<h3 id="subdirectories-and-runtime-environments">Subdirectories and runtime environments<a href="#subdirectories-and-runtime-environments" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>In real projects, subdirectories often need their own <code>.envrc</code> — for example to activate a conda environment. Because <code>direnv</code> executes only the <em>nearest</em> <code>.envrc</code>, inheritance must be explicit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># sub-project/.envrc</span>
</span></span><span class="line"><span class="cl">source_up
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/miniforge3/etc/profile.d/conda.sh
</span></span><span class="line"><span class="cl">conda activate <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/.conda&#34;</span>
</span></span></code></pre></div><p>This ensures secrets loaded at the project root remain available while runtime configuration stays local.</p>
<hr>
<h2 id="pattern-2-command-scoped-injection-one-off-usage">Pattern 2: Command-scoped injection (one-off usage)<a href="#pattern-2-command-scoped-injection-one-off-usage" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>This pattern answers a different question: <em>&ldquo;I just want to run this command once with specific credentials.&rdquo;</em></p>
<p>Sometimes there is no project context. You just want to test an API, run a script, or make a quick request. For this case, inject secrets for a single command using <code>op run</code>. Secrets exist <strong>only for the lifetime of that process</strong>.</p>
<h3 id="using-op-run-with-environment-ids">Using <code>op run</code> with Environment IDs<a href="#using-op-run-with-environment-ids" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Since February 2026, <code>op run</code> supports loading variables directly from a 1Password Environment using the <code>--environment</code> flag. This is cleaner than maintaining separate FIFO mount files for ad-hoc use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Find your Environment ID in 1Password:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Developer → View Environments → Manage environment → Copy environment ID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">op run --environment env_1234567890abcdef -- curl -s https://api.openai.com/v1/models
</span></span></code></pre></div><blockquote>
<p><strong>Stdout masking.</strong> By default, <code>op run</code> monitors stdout and stderr and automatically conceals any secret values that appear in output. If you need to see the actual values (e.g. for debugging), pass the <code>--no-masking</code> flag. In production scripts, leave masking on.</p></blockquote>
<h3 id="using-central-fifo-mounts-alternative">Using central FIFO mounts (alternative)<a href="#using-central-fifo-mounts-alternative" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>If you prefer not to pass Environment IDs, you can maintain central FIFO mounts and reference them by path:</p>
<pre tabindex="0"><code>~/.envs/dev-work.env
~/.envs/dev-personal.env
</code></pre><p>Then inject via:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">op run --env-file ~/.envs/dev-work.env -- &lt;command&gt;
</span></span></code></pre></div><h3 id="convenience-wrapper">Convenience wrapper<a href="#convenience-wrapper" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>For daily use, a small shell function keeps things fast:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ~/.zshrc</span>
</span></span><span class="line"><span class="cl">dev-env<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">which</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span><span class="p">;</span> <span class="nb">shift</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> env_id
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$which</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">    work<span class="o">)</span>     <span class="nv">env_id</span><span class="o">=</span><span class="s2">&#34;env_work_1234567890&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">    personal<span class="o">)</span> <span class="nv">env_id</span><span class="o">=</span><span class="s2">&#34;env_personal_abcdef01&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">    *<span class="o">)</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;usage: dev-env {work|personal} &lt;command...&gt;&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">      <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">esac</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  op run --environment <span class="s2">&#34;</span><span class="nv">$env_id</span><span class="s2">&#34;</span> -- zsh -l -c <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Usage:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dev-env work <span class="s1">&#39;curl -s https://api.openai.com/v1/models&#39;</span>
</span></span></code></pre></div><p>Secrets exist only for the lifetime of that command and never pollute the shell.</p>
<hr>
<h2 id="pattern-3-programmatic-sdk-access">Pattern 3: Programmatic SDK access<a href="#pattern-3-programmatic-sdk-access" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>For applications that need secrets at runtime — not just in shell environments — 1Password now offers native SDKs in Go, Python, and JavaScript. This is particularly useful for scripts, internal tools, or CI pipelines that need to fetch credentials on demand.</p>
<h3 id="reading-an-environment-with-the-cli">Reading an Environment with the CLI<a href="#reading-an-environment-with-the-cli" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>The <code>op environment read</code> command returns all variables from an Environment as key-value pairs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Read all variables from an Environment</span>
</span></span><span class="line"><span class="cl">op environment <span class="nb">read</span> env_1234567890abcdef
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pipe to other tools</span>
</span></span><span class="line"><span class="cl">op environment <span class="nb">read</span> env_1234567890abcdef <span class="p">|</span> grep API_KEY
</span></span></code></pre></div><h3 id="reading-an-environment-with-the-python-sdk">Reading an Environment with the Python SDK<a href="#reading-an-environment-with-the-python-sdk" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">onepassword</span> <span class="kn">import</span> <span class="n">Client</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">Client</span><span class="o">.</span><span class="n">authenticate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">integration_name</span><span class="o">=</span><span class="s2">&#34;my-script&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">integration_version</span><span class="o">=</span><span class="s2">&#34;0.1.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fetch all variables from a 1Password Environment</span>
</span></span><span class="line"><span class="cl"><span class="n">env_id</span> <span class="o">=</span> <span class="s2">&#34;env_1234567890abcdef&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">environments</span><span class="o">.</span><span class="n">get_variables</span><span class="p">(</span><span class="n">env_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">var</span> <span class="ow">in</span> <span class="n">response</span><span class="o">.</span><span class="n">variables</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">var</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="s1">&#39;*****&#39;</span> <span class="k">if</span> <span class="n">var</span><span class="o">.</span><span class="n">hidden</span> <span class="k">else</span> <span class="n">var</span><span class="o">.</span><span class="n">value</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></div><blockquote>
<p><strong>Local authentication.</strong> The SDK can authenticate through the 1Password desktop app using biometrics or your account password. No service account tokens needed for local development. This is a human-in-the-loop approval model — 1Password prompts you for consent, then the SDK receives a scoped session.</p></blockquote>
<h2 id="when-to-use-which-pattern">When to use which pattern<a href="#when-to-use-which-pattern" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>Best for</th>
          <th>Secret lifetime</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Project mount</strong> (FIFO + direnv)</td>
          <td>Day-to-day development in a specific project</td>
          <td>While in directory</td>
      </tr>
      <tr>
          <td><strong>Command injection</strong> (<code>op run</code>)</td>
          <td>One-off API calls, quick tests, CI steps</td>
          <td>Single command</td>
      </tr>
      <tr>
          <td><strong>SDK / CLI read</strong></td>
          <td>Internal tools, automation, Python/Go/JS apps</td>
          <td>Application-controlled</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="why-direnv-is-optional-but-powerful">Why <code>direnv</code> is optional but powerful<a href="#why-direnv-is-optional-but-powerful" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Nothing in this design <em>requires</em> <code>direnv</code>. Projects can read <code>.env</code> directly, command-scoped injection works independently, and the SDK pattern bypasses the shell entirely.</p>
<p>However, <code>direnv</code> dramatically improves the developer experience by managing <strong>shell-level visibility</strong>:</p>
<ul>
<li>Enter directory → variables appear.</li>
<li>Leave directory → variables disappear.</li>
</ul>
<p>This keeps the shell clean while remaining convenient.</p>
<p>The important point is that <strong>direnv is a lifecycle helper, not a secret manager</strong>. It never stores secrets; it only executes logic. The <code>.envrc</code> file itself can be safely committed to version control because it contains no secrets — only the instruction to load <code>.env</code>.</p>
<hr>
<h2 id="known-limitations--caveats">Known limitations &amp; caveats<a href="#known-limitations--caveats" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The 1Password Environments feature is still in beta, and there are some sharp edges worth knowing about.</p>
<h3 id="concurrency">Concurrency<a href="#concurrency" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>FIFO-mounted <code>.env</code> files are not designed for concurrent access. If multiple processes try to read the FIFO simultaneously — for example, an IDE and a terminal session — only the first reader succeeds. Others may hang or return empty data.</p>
<blockquote>
<p><strong>Practical impact.</strong> If you have your <code>.env</code> open in an IDE (some will auto-read it for IntelliSense), other applications may fail to read the same file. Close the IDE&rsquo;s file handle or use <code>op run --environment</code> as a workaround for the second consumer.</p></blockquote>
<h3 id="platform-support-limits-and-desktop-app-dependency">Platform support, limits, and desktop app dependency<a href="#platform-support-limits-and-desktop-app-dependency" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Local <code>.env</code> file mounts are currently supported on <strong>Mac and Linux only</strong>. Windows support is being explored but is not yet available; on Windows, use <code>op run --environment</code> or the SDK instead. On all platforms, you can have up to <strong>10 enabled local <code>.env</code> file mounts per device</strong>. For most workflows this is sufficient, but if you manage many microservices locally, you may need to combine some Environments or lean on the CLI/SDK patterns. All of this also depends on the 1Password desktop app being running and unlocked — these mechanisms are strictly developer workstation features, not something to run on servers or headless CI. For CI/CD, use <a href="https://developer.1password.com/docs/service-accounts">1Password Service Accounts</a> instead.</p>
<h3 id="ai-agent-integration">AI agent integration<a href="#ai-agent-integration" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>If you use AI-assisted IDEs like Cursor or GitHub Copilot, 1Password provides an <a href="https://developer.1password.com/docs/environments/agent-hook-validate/">agent hook</a> that validates your FIFO mounts before the agent executes shell commands. You can configure which files to validate using a <code>.1password/environments.toml</code> file at the project root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># .1password/environments.toml</span>
</span></span><span class="line"><span class="cl"><span class="nx">mount_paths</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;.env&#34;</span><span class="p">]</span>
</span></span></code></pre></div><p>The hook prevents command execution if any required <code>.env</code> FIFO is missing or disabled, which keeps the agent from running against misconfigured environments.</p>
<hr>
<h2 id="final-notes">Final notes<a href="#final-notes" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Once this workflow is in place, the benefits compound quickly. Key rotation becomes straightforward: rotate in 1Password, and consumers pick it up on the next read, with no find-and-replace across scattered files. Environment switching is explicit and reversible, whether you remount a different Environment for a project or change the Environment ID in a wrapper command. Just as importantly, secrets avoid the common leak paths: FIFO mounts do not write plaintext to disk, <code>op run</code> helps conceal secret values in command output, and <code>direnv</code> removes variables when you leave a project directory.</p>
<p>The operational impact is bigger than just security posture. Onboarding gets simpler because you share Environment access instead of sending <code>.env</code> files around, and auditing improves because access logs exist by default. Over time, the mental overhead drops: you stop &ldquo;remembering where keys are&rdquo; and start relying on a system with explicit lifetimes.</p>
<p>At a design level, this is the main takeaway: treat local secret handling as an engineering system, not a collection of ad-hoc habits. The specific tools can change, but the principles hold — one source of truth, scoped injection, and clear lifecycle control. If you already use 1Password Environments, this workflow aligns with the feature&rsquo;s strengths; if you do not, the same pattern still applies to other secret managers.</p>
]]></content></item><item><title>Self-host Deployment of RSSHub</title><link>https://blog.colemei.com/posts/self-host-deployment-of-rsshub/</link><pubDate>Mon, 11 Sep 2023 23:50:36 +0800</pubDate><author>Cole Mei</author><guid>https://blog.colemei.com/posts/self-host-deployment-of-rsshub/</guid><description>&lt;h2 id="引言"&gt;引言&lt;a href="#%e5%bc%95%e8%a8%80" class="anchor" aria-hidden="true"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="feather"&gt;
&lt;path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"&gt;&lt;/path&gt;
&lt;line x1="8" y1="12" x2="16" y2="12"&gt;&lt;/line&gt;
&lt;/svg&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;说起来，我采用「RSS信息源 + 一款心仪的多平台阅读器」这样的方式获取信息已经有一年多的时间了。总的来说，它带给了我很不错体验，有效的缓解了我在Daily Input过程中的一些无奈与焦虑：一方面，它使筛选过的优质信息源尽量常驻，另一方面，它使阅读信息的过程尽量纯粹。&lt;/p&gt;</description><enclosure url="https://blog.colemei.com/695-1920x1080_7272941181790737725.jpg" length="" type="image/jpeg"/><media:thumbnail url="https://blog.colemei.com/695-1920x1080_7272941181790737725.jpg" width="1920" height="1080"/><media:content url="https://blog.colemei.com/695-1920x1080_7272941181790737725.jpg" medium="image" type="image/jpeg" width="1920" height="1080"><media:title type="html">/695-1920x1080_7272941181790737725.jpg</media:title></media:content><content type="text/html" mode="escaped"><![CDATA[<h2 id="引言">引言<a href="#%e5%bc%95%e8%a8%80" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>说起来，我采用「RSS信息源 + 一款心仪的多平台阅读器」这样的方式获取信息已经有一年多的时间了。总的来说，它带给了我很不错体验，有效的缓解了我在Daily Input过程中的一些无奈与焦虑：一方面，它使筛选过的优质信息源尽量常驻，另一方面，它使阅读信息的过程尽量纯粹。</p>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/Feed-icon.svg.png" alt="image"
            decoding="async" /><figcaption><p>RSS (Stand For Really Simple Syndication)</p></figcaption></figure>
<p>其实，RSS背后是由一套挺反商业的逻辑在支持，也因此，很多平台和媒体都并不提供官方的RSS订阅链接，毕竟如果每个人都在自己的一隅空间内，不去访问门户网站页面，那么用户的数据偏好从哪里得到？铺天盖地的广告往哪里投送？</p>
<p>这样的协议无疑站在了消费主义的对立面。所以，刚才提到的一切想法得以实现的一个理论基底，就是<a href="https://github.com/DIYgod/RSSHub">RSSHub</a>这个伟大的开源项目的存在。关于RSSHub，官方docs中作了如下介绍:</p>
<blockquote>
<p>RSSHub 是一个开源、简单易用、易于扩展的 RSS 生成器，可以给任何奇奇怪怪的内容生成 RSS 订阅源。RSSHub 借助于开源社区的力量快速发展中，目前已适配数百家网站的上千项内容</p>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/rsshub-intro.png" alt="image"
            decoding="async" /><figcaption><p>RSSHub</p></figcaption></figure></blockquote>
<p>一言以蔽之，很多并没有提供RSS功能的平台与媒体，通过RSSHub的Route(路由)，或者更简单的，利用其Radar插件，可以很方便的生成订阅源。</p>
<p>更多关于RSS以及RSSHub的介绍就不在这里展开了，下面切入正题：</p>
<hr>
<h2 id="无尽的折腾">无尽的折腾<a href="#%e6%97%a0%e5%b0%bd%e7%9a%84%e6%8a%98%e8%85%be" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<h3 id="1-需求">1. 需求<a href="#1-%e9%9c%80%e6%b1%82" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>这次折腾之旅的起因是这样的：我经常阅读的一个RSS订阅源-<a href="https://theinitium.com/">端传媒</a>，由于网站的改版与代码重构，原本的源不能获取到文章的全部内容了，需要跳转到原链接去阅读。而网站上的文章文字显示为繁体，且字体看起来不顺眼不说，文章周围还夹杂着很多乱七八糟的内容，很是无奈。</p>
<p>花着高昂的订阅费，却得不到称心如意的阅读体验，这还得了(也不得不感叹这些对于体验的改造工作居然要消费者来承担)？于是遂至RSSHub docs的相关路由路径下查看，发现如下字样：</p>
<blockquote>
<p>付费内容全文可能需要登陆获取，详情见部署页面的配置模块。</p></blockquote>
<p>也是我第一次了解到原来RSSHub的一整套服务还可以自己部署，至于自建的原因，我是这样理解的：</p>
<ol>
<li>由RSSHub的官方域名路径构成的源多数都因被污染、被反制、被滥用等等原由而无法使用。</li>
<li>随着很多网站的政策更新，获取其内容所需要的条件越来越苛刻，像是API、Cookies、Token等信息需要预配置。</li>
</ol>
<p>好在RSSHub的生态还算完整，对于自建这样一个相对小众的需求也有比较详细（当然还不够！不然我也不写这篇blog了）的文档支持。</p>
<h1 id="hahahugoshortcode29s2hbhb">

<p style="text-align:center">折腾开始！</p>
<a href="#hahahugoshortcode29s2hbhb" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h1>
<h3 id="2-基本部署">2. 基本部署<a href="#2-%e5%9f%ba%e6%9c%ac%e9%83%a8%e7%bd%b2" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>官方提供的Guideline中有很多种部署方式，我尝试了三种：Docker-Compose部署、HeroKu部署、Vercel部署。最后顺利完成自建是使用的Vercel。</p>
<ul>
<li>
<h4 id="docker-compose">Docker-compose<a href="#docker-compose" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h4>
</li>
</ul>
<p>9月5日，我开始了第一次尝试。</p>
<p>由于我这一阵子刚刚学完Mosh的<a href="https://www.bilibili.com/video/BV1pf4y1W7YA/">Ultimate Docker Course</a>，而古人云：一个人刚刚掌握了一项新的工具后，当他发现能将它立马用到解决自己遇到的实际问题，这无疑是一件很令人激动的事情。于是当我看到支持中有Docker字样的instance时，眼睛放出的光芒四射，我毫不犹豫的优先选择了它。</p>
<p>部署的过程不算困难，只需要在Terminal中零星敲上几个指令，就能在本地端口1200下看到“Welcome to RSSHub”的字样，还蛮顺利的。</p>
<p>运行docker的相关命令，发现：images成功运行在本地，volumes也没有忘记创建，（嗯，我Docker学的没问题！）但是不知道是不是部署在本地的原因，真正将自建的服务用到订阅源的构建时，基本全是报错。只有少数的一些静态page或是比较简单的网页能够成功的解析。碍于我在网络方面的知识储备相当匮乏，在持续扑腾了两个小时以后，我还是像一只沮丧的旱鸭子一样放弃了，并不甘心的在我的LogSeq的Daily Journal中作了如下记录：</p>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/logseq-journal-sep-5th.png" alt="image"
            decoding="async" /><figcaption><p>LogSeq-Journal Page-Sep 5th</p></figcaption></figure>
<ul>
<li>
<h4 id="heroku">HeroKu<a href="#heroku" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h4>
</li>
</ul>
<p>时间来到了9月7日，这天我也是经历了挺疯狂的一晚。</p>
<p>我千方百计的想要疯狂榨干Github Student Package中的每一滴价值，从下午三点开始把自己埋在电脑前一直到了凌晨三点：从零开始完成了两个Vps的部署（这个有机会也可以另起一篇记录下），申请并注册了个人的第二个domain，学习了使用Termius、浏览了上百个webpage……</p>
<p>我将在自己身上时有发生的这种好像进入了某种Zone的状态戏称为“Geeky Time”，也算是另一种比较nerd的“Kenja Taimu”吧。</p>
<p>哈哈哈，好像跑题了。</p>
<p>话说回来，在Student Package中，我瞥见了Heroku的字样，它也是RSSHub官方支持的一种自建方式，且支持一键部署，但无奈其不提供免费服务，每月的额度也十分有限。这不是和我的学生包中提供的条目完美匹配吗？于是我尝试了注册Heroku，但是倒在了最后一步：没能添加正确的Payment methods，也真是挺奇怪的，我的这张Monai卡陪我注册和订阅过大大小小很多个境外服务，怎么到你这就不行了，无奈作罢。</p>
<p>后来分析了下，可能Heroku需要的是国外的本地卡作为支付方式吧，而不是我这样一张简单的Mastercard外币卡。</p>
<ul>
<li>
<h4 id="vercel">Vercel<a href="#vercel" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h4>
</li>
</ul>
<p>最后是Vercel，俗话说好饭不怕晚。其实早在部署我的blog时，我就有体验过Vercel的托管服务，但因为一些原因（一切从简与轻量化）还是选择了Github Pages。基于Vercel的部署过程体验下来还算简单，RSSHub的官方Docs中提供了和Heroku一样的一键部署功能，但这里我还是选择了（也更推荐）带有自动更新的部署方式：</p>
<ol>
<li>在Github中：将RSSHubFork到自己的repo中。</li>
<li>在Vercel中：
<ol>
<li>完成对Github Account的绑定。</li>
<li>新建Project，关联Github中的对应repo并导入。</li>
<li>按需自定义修改相关设置，等待Build完成</li>
</ol>
</li>
</ol>
<p>更详细的部署教程（step by step）可以参考这篇Guide：<a href="https://www.cnblogs.com/QiuSYan/p/RssHub.html">RssHub + Vercel ：在 Vercel 上免费部署你的RssHub</a>。</p>
<p>至此，专属于我自己的RSSHub就已经部署在了Vercel为我提供的Domain下，点击visit即可看到Welcome Page。</p>
<h1 id="hahahugoshortcode29s4hbhb">

<p style="text-align:center">Bingo！</p>
<a href="#hahahugoshortcode29s4hbhb" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h1>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/deploy-success.png" alt="image"
            decoding="async" /><figcaption><p>部署成功！</p></figcaption></figure>
<h3 id="3-自定义配置">3. 自定义配置<a href="#3-%e8%87%aa%e5%ae%9a%e4%b9%89%e9%85%8d%e7%bd%ae" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>但是到此我只实现了刚才提到的“为什么要自建”的第一个理由，我获得了一个没有那么多人同时使用的相对私人的带有RSSHub功能的域名服务，可以把相关路由都迁移过来，生成对应的大部分订阅源。但我还是没有办法获取那些需要特定登陆信息才能完成信息提取的网站，像是Youtube、Bilibili、Pixiv、Instagram等等，当然也包括我的issue：端传媒。</p>
<ul>
<li>
<h4 id="配置environment-variables">配置Environment variables<a href="#%e9%85%8d%e7%bd%aeenvironment-variables" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h4>
</li>
</ul>
<p>参考RSSHub的<a href="https://docs.rsshub.app/zh/install#%E9%83%A8%E5%88%86-rss-%E6%A8%A1%E5%9D%97%E9%85%8D%E7%BD%AE">docs</a>和<a href="https://github.com/DIYgod/RSSHub/blob/master/lib/config.js">code</a>，我得知我需要为路由指定那些所需的特殊values。基于我选择的托管服务Vercel，这里可以采取两种方法：</p>
<ol>
<li>
<p>在Github的repo中的根目录下新建一个 <code>.env</code> 文件，每行以 <code>NAME=VALUE</code> 格式添加环境变量，例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">CACHE_TYPE</span><span class="o">=</span>redis
</span></span><span class="line"><span class="cl"><span class="nv">CACHE_EXPIRE</span><span class="o">=</span><span class="m">600</span>
</span></span></code></pre></div></li>
<li>
<p>在Vercel的Dashboard中，点击settings，在左侧栏目中选择Enviroment variables，填入对应的Keys和Values并Save。</p>
</li>
</ol>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/vercel-env-vars-1.png" alt="image"
            decoding="async" /><figcaption><p>成对存在的环境变量-1</p></figcaption></figure>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/vercel-env-vars-2.png" alt="image"
            decoding="async" /><figcaption><p>成对存在的环境变量-2</p></figcaption></figure>
<p>这里我选择的是第二种方法。另外值得注意的是，在每次添加新的或是修改现有的Variables后，都要Redeploy该Project才能使这些参数得以在路由中生效。</p>
<ul>
<li>
<h4 id="配置custom-domains">配置Custom Domains<a href="#%e9%85%8d%e7%bd%aecustom-domains" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h4>
</li>
</ul>
<p>如果你觉得Vercel给你分配的域名不太顺眼（且这个xxx.vercel.app好像是被默认墙的），那么你可以将它部署到自己的域名中。上文提到我手里正好有一个闲置的域名（也是从Github Student Package中薅的羊毛 ^o^，来自Name.com的免费一年的.live域名），于是就将Vercel的服务redirect到了我的这个域名上。相关操作非常简单，按部就班follow好vercel上的引导即可：</p>
<ol>
<li>添加域名</li>
<li>在域名的DNS解析服务商（我的是Cloudflare）处添加相应的DNS记录</li>
<li>等待redirect成功</li>
</ol>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/vercel-custom-domain.png" alt="image"
            decoding="async" /><figcaption><p>自定义域名</p></figcaption></figure>
<h3 id="4-成果">4. 成果<a href="#4-%e6%88%90%e6%9e%9c" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>至此，已经实现了Self-Host的全部步骤，此时，可以在RSSHub中的官方路由随意挑选几个，将其中域名 <code>https://rsshub.app</code> 的部分替换为我刚才成功自部署的域名，添加到阅读器中看看是否可以正常使用。如果你也同样和我一样使用<strong>RSSHub Radar</strong>的话，别忘了在设置中修改自定义RSSHub域名，像是这样：</p>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/rsshub-radar.png" alt="image"
            decoding="async" /><figcaption><p>RSSHub Radar</p></figcaption></figure>
<p>回到我心爱的阅读器Reeder中验证一下，在新的订阅源中，看看端传媒的文章能否完整输出呢？</p>





<figure class=""><img src="https://raw.githubusercontent.com/ColeMei/Picgo/master/self-host-deployment-of-rsshub/final-verification.gif" alt="video"
            decoding="async" /><figcaption><p>最后的验证</p></figcaption></figure>
<p>可以看到，上面的GIF中，我先点击的是自建域名下生成的新订阅中的文章（全文），后点击的是老的订阅源中的文章（部分）</p>
<h1 id="hahahugoshortcode29s11hbhb">

<p style="text-align:center">Perfect！</p>
<a href="#hahahugoshortcode29s11hbhb" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h1>
<hr>
<h2 id="结语">结语<a href="#%e7%bb%93%e8%af%ad" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>这篇blog算是对又一次的折腾之旅的一次小记录，并不能称之为一篇正经的教程，所以更多的记录的是我的一些思辨过程，写一些我想写的而不是简单的step by step的tracking。</p>
<p>最近这段时间日子过的比较清闲，在做出了可能是人生中的又一个Big Decision后，短暂结束了自己的迷茫与百无聊赖，折腾的频率就也又上来了。</p>
<p>今天难得心血来潮，洋洋洒洒写成此篇，就借此以最美好的祝愿许给RSS（Really Simple Syndication）这一古老朴素而余温无限的协议和其繁盛的生态以最高的敬意吧！</p>
]]></content></item></channel></rss>