<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rbcn</title>
    <description>The latest articles on DEV Community by rbcn (@rbcn).</description>
    <link>https://dev.to/rbcn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960223%2F49e53741-5124-4a60-abb8-bdfc16c8d509.png</url>
      <title>DEV Community: rbcn</title>
      <link>https://dev.to/rbcn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rbcn"/>
    <language>en</language>
    <item>
      <title>I Built a Pipeline to Publish This Post.</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Sat, 30 May 2026 18:25:54 +0000</pubDate>
      <link>https://dev.to/rbcn/i-built-a-pipeline-to-publish-this-post-5fij</link>
      <guid>https://dev.to/rbcn/i-built-a-pipeline-to-publish-this-post-5fij</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;I write in Obsidian. I publish on Hashnode. Until recently, moving between the two looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the vault note&lt;/li&gt;
&lt;li&gt;Copy the content&lt;/li&gt;
&lt;li&gt;Open Hashnode editor&lt;/li&gt;
&lt;li&gt;Paste and reformat the frontmatter manually&lt;/li&gt;
&lt;li&gt;Hunt for a cover image&lt;/li&gt;
&lt;li&gt;Upload it&lt;/li&gt;
&lt;li&gt;Hit publish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Somewhere between ten and fifteen minutes of friction between "I'm done writing" and "the post is live."&lt;/p&gt;

&lt;p&gt;I got tired of it. So I built a pipeline.&lt;/p&gt;

&lt;p&gt;This post documents that pipeline — and it was published through it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem with Manual Publishing
&lt;/h3&gt;

&lt;p&gt;My Obsidian notes have their own frontmatter schema. Hashnode has its own. They're similar enough to feel like they should be the same thing, and different enough that copy-pasting always broke something.&lt;/p&gt;

&lt;p&gt;The tags format was different. The title needed quoting sometimes and not others. The cover image had to be uploaded separately. The &lt;code&gt;## 📝 Draft&lt;/code&gt; section header had to be stripped out.&lt;/p&gt;

&lt;p&gt;None of these are hard problems. But they were the kind of small, repetitive friction that makes you skip publishing altogether.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;The pipeline has three components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04_Publish note  (Obsidian vault)
  ↓
/hashnode-publish  (Claude Code skill)
  converts frontmatter, generates slug, writes file
  ↓
post/&amp;lt;timestamp&amp;gt;/slug.md  (GitHub relay repo)
  ↓  git push → GitHub Actions
  ├─ Resolve Unsplash cover image
  └─ Hashnode API  →  live post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a single responsibility. The vault stays clean. The publishing logic lives outside it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Component 1: The GitHub Relay Repo
&lt;/h3&gt;

&lt;p&gt;The first piece is a GitHub repository whose only job is to receive markdown files and forward them to Hashnode.&lt;/p&gt;

&lt;p&gt;The GitHub Actions workflow triggers on any push that touches &lt;code&gt;post/**/*.md&lt;/code&gt;. The nested path pattern matters — &lt;code&gt;post/*.md&lt;/code&gt; wouldn't match, so every article goes into a timestamp-named subfolder: &lt;code&gt;post/20260517143000/slug.md&lt;/code&gt;. That folder name also serves as a natural ordering key.&lt;/p&gt;

&lt;p&gt;When the workflow fires, it runs two steps per changed file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolve Unsplash cover image&lt;/strong&gt; — more on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hashnode publish&lt;/strong&gt; — calls the Hashnode API via &lt;code&gt;raunakgurud09/hashnode-publish@v1.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Hashnode API key lives in GitHub Secrets. It never touches the local machine.&lt;/p&gt;




&lt;h3&gt;
  
  
  Component 2: The Claude Code Skill
&lt;/h3&gt;

&lt;p&gt;The second piece is a Claude Code slash command: &lt;code&gt;/hashnode-publish&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When invoked, it reads the current vault note, parses its frontmatter, and converts it to Hashnode's format. Then it generates a slug from the filename, creates the timestamp folder, writes the converted file, and pushes to the relay repo.&lt;/p&gt;

&lt;p&gt;The frontmatter conversion handles the differences between the vault schema and Hashnode's schema: tag format, field ordering, stripping vault-specific fields, dropping the &lt;code&gt;## 📝 Draft&lt;/code&gt; header from the body. If &lt;code&gt;description&lt;/code&gt; is empty, it stops and asks to fill it in first. If &lt;code&gt;publish&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, it warns before proceeding.&lt;/p&gt;

&lt;p&gt;From the writer's perspective, the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Write the note in Obsidian
2. Fill in the frontmatter (title, description, tags, etc.)
3. Type /hashnode-publish
4. Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire publishing process — conversion, file creation, commit, push — runs in under ten seconds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Component 3: Unsplash Cover Images
&lt;/h3&gt;

&lt;p&gt;The third piece solves the cover image problem.&lt;/p&gt;

&lt;p&gt;The vault frontmatter accepts a &lt;code&gt;splash_keyword&lt;/code&gt; field. When it's set and &lt;code&gt;cover_image&lt;/code&gt; is empty, the skill passes the keyword through to the relay repo file. GitHub Actions picks it up, calls the Unsplash API, and injects the result as &lt;code&gt;cover_image&lt;/code&gt; before passing the file to Hashnode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In the vault note frontmatter&lt;/span&gt;
&lt;span class="na"&gt;splash_keyword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;workspace"&lt;/span&gt;
&lt;span class="na"&gt;cover_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions:
  splash_keyword found → GET /photos/random?query=developer+workspace
  → cover_image: https://images.unsplash.com/photo-...
  → splash_keyword removed from frontmatter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Unsplash API key lives in GitHub Secrets alongside the Hashnode key. Nothing sensitive is stored locally.&lt;/p&gt;

&lt;p&gt;One more thing: the Unsplash API terms encourage attribution. So whenever an image is fetched, the workflow appends a line to the article body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="ge"&gt;*cover image: [unsplash](https://unsplash.com/)*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cover image for this post was chosen by that step. I wrote &lt;code&gt;splash_keyword: "developer workspace"&lt;/code&gt; and the algorithm did the rest.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Decision to Use a Relay Repo
&lt;/h3&gt;

&lt;p&gt;A simpler design would call the Hashnode API directly from the Claude Code skill. No relay repo, no GitHub Actions — just a local script with the API key.&lt;/p&gt;

&lt;p&gt;I chose the relay repo approach for one reason: &lt;strong&gt;the API keys live in GitHub Secrets, not on the local machine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This matters more than it might seem. A Claude Code skill runs with access to the local environment. Keeping secrets out of that environment, and inside a CI/CD system with audit logs and rotation tooling, is a better default. The relay repo adds one step but removes a category of risk.&lt;/p&gt;

&lt;p&gt;There's a secondary benefit: the relay repo is a record. Every published post has a corresponding commit with a timestamp and a slug. That's useful.&lt;/p&gt;




&lt;h3&gt;
  
  
  What the Frontmatter Looks Like
&lt;/h3&gt;

&lt;p&gt;A complete vault note ready for publishing looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Post&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Title."&lt;/span&gt;
&lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subtitle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;here."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag2"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;two&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sentences&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;describing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;post."&lt;/span&gt;
&lt;span class="na"&gt;cover_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="na"&gt;splash_keyword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relevant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;term"&lt;/span&gt;
&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;enableTableOfContent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;isNewsletterActivated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields drive the cover image behavior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;cover_image&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;splash_keyword&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Use the URL directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;Fetch from Unsplash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;No cover image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Lessons
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The friction between "done writing" and "published" is a publishing problem, not a writing problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Optimizing the writing environment — better tools, better structure — doesn't help if the last mile is still manual. The pipeline had to be built separately.&lt;/p&gt;

&lt;p&gt;And a pattern that recurred across all three components:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Put secrets where they belong. API keys in GitHub Secrets, not environment variables, not shell configs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's the same principle that came up in the SSH agent post: scope matters. A credential should only be reachable from the system that actually needs to use it.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;The pipeline is functional. A few things I'd add eventually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update existing posts&lt;/strong&gt;: the current workflow only handles new files; updating a published post requires a separate flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview step&lt;/strong&gt;: see the formatted output before pushing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Japanese articles&lt;/strong&gt;: the same pipeline works, but slug generation from Japanese filenames needs a romanization step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, typing &lt;code&gt;/hashnode-publish&lt;/code&gt; and watching the commit appear in the relay repo is satisfying enough.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>hashnode</category>
      <category>githubactions</category>
      <category>claude</category>
    </item>
    <item>
      <title>Migrating to zsh Broke Obsidian Git.</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Sat, 30 May 2026 17:53:20 +0000</pubDate>
      <link>https://dev.to/rbcn/migrating-to-zsh-broke-obsidian-git-2blj</link>
      <guid>https://dev.to/rbcn/migrating-to-zsh-broke-obsidian-git-2blj</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;In the previous post, I quit &lt;code&gt;.bashrc&lt;/code&gt; and gave bash and zsh separate responsibilities. The terminal environment was clean.&lt;/p&gt;

&lt;p&gt;The next day, I opened Obsidian and Git sync had stopped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looked like "switching to zsh broke Obsidian." But the shell wasn't the real culprit.&lt;br&gt;
&lt;strong&gt;The ssh-agent's scope simply wasn't reaching GUI applications.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the record of how I tracked down that cause and fought my way through the Flatpak sandbox to fix it.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Structure of the Problem
&lt;/h3&gt;

&lt;p&gt;Checking from the terminal, the SSH keys were visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# /run/user/1000/gcr/ssh&lt;/span&gt;
ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;              &lt;span class="c"&gt;# work-github / personal-github&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yet Obsidian Git plugin kept asking for a passphrase every single time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal (zsh) → OK
Obsidian (GUI) → NG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem broke down like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-agent launched from a shell
  → only valid within that shell's process tree

Obsidian launched by the GUI
  → never reads .zshrc
  → never reaches the ssh-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, &lt;strong&gt;this wasn't a shell configuration problem — it was a login session design problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Did It Work Before?
&lt;/h3&gt;

&lt;p&gt;A question immediately surfaced: when everything was crammed into &lt;code&gt;.bashrc&lt;/code&gt;, Obsidian Git worked fine. Why?&lt;/p&gt;

&lt;p&gt;The answer turned out to involve GNOME Keyring (gcr).&lt;/p&gt;

&lt;p&gt;gcr-ssh-agent integrates with the GNOME login session and automatically exposes SSH keys when the login keyring is unlocked. That meant Obsidian had always been talking to gcr directly, without going through the shell at all.&lt;/p&gt;

&lt;p&gt;It wasn't that passphrases were &lt;em&gt;unnecessary&lt;/em&gt; — it was that &lt;strong&gt;GNOME Keyring had been silently providing the previously-saved passphrase all along&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempting gcr-ssh-agent Unification (and Failing)
&lt;/h3&gt;

&lt;p&gt;My first plan was to leverage this mechanism and route everything through gcr.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcr-ssh-agent
  → /run/user/1000/gcr/ssh
  → shared by Terminal / Editor / Obsidian / Flatpak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I exposed the gcr socket to the Flatpak Obsidian sandbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flatpak override &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filesystem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xdg-run/gcr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000/gcr/ssh &lt;span class="se"&gt;\&lt;/span&gt;
  md.obsidian.Obsidian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked — temporarily. GitHub authentication went through.&lt;/p&gt;

&lt;p&gt;Then I rebooted. &lt;strong&gt;The problem came back.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Signing hangs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;              &lt;span class="c"&gt;# personal-github / work-github → visible&lt;/span&gt;
ssh &lt;span class="nt"&gt;-T&lt;/span&gt; github-personal  &lt;span class="c"&gt;# → hangs (no response)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verbose log showed GitHub &lt;em&gt;was&lt;/em&gt; accepting the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offering public key: id_ed25519_personal
Server accepts key: id_ed25519_personal
signing using ssh-ed25519   ← hangs here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signing request sent to gcr-ssh-agent was never coming back.&lt;/p&gt;

&lt;h4&gt;
  
  
  Keys resurrect after &lt;code&gt;ssh-add -D&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add &lt;span class="nt"&gt;-D&lt;/span&gt;   &lt;span class="c"&gt;# All identities removed.&lt;/span&gt;
ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;   &lt;span class="c"&gt;# personal-github / work-github  ← instantly back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gcr was auto-publishing keys from its keyring storage. And that auto-restored state was broken for signing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cleaning up via Seahorse was risky
&lt;/h4&gt;

&lt;p&gt;When I tried to delete SSH key entries in Seahorse (GNOME's keyring manager), I realized it looked like it would &lt;strong&gt;delete the actual private key files&lt;/strong&gt; (&lt;code&gt;~/.ssh/id_ed25519_personal&lt;/code&gt;) along with them.&lt;/p&gt;

&lt;p&gt;That was the end of the gcr unification attempt.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Remained Unexplained
&lt;/h3&gt;

&lt;p&gt;I never found the root cause of gcr-ssh-agent's signing hang. Here's what I was able to rule out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The key files were not corrupted
The public keys on GitHub were registered correctly
Exposing the Flatpak socket was not the sole cause
~/.ssh/config Host aliases were not misconfigured

→ gcr-ssh-agent's auto-restored state was unstable at signing time
→ Root cause: unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a verdict that gcr-ssh-agent is broken. This problem happened in this environment. Given that, I chose to prioritize determinism and switch to OpenSSH ssh-agent.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pinning OpenSSH ssh-agent as a systemd User Service
&lt;/h3&gt;

&lt;p&gt;I decided to manage a plain OpenSSH ssh-agent with a fixed socket path under systemd user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/systemd/user

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/systemd/user/ssh-agent.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=OpenSSH key agent

[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a %t/ssh-agent.socket

[Install]
WantedBy=default.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; ssh-agent.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The socket is now pinned at &lt;code&gt;/run/user/1000/ssh-agent.socket&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Updating &lt;code&gt;~/.ssh/config&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;I added explicit &lt;code&gt;IdentityAgent&lt;/code&gt; directives so SSH connections don't depend on the current value of &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes
    AddKeysToAgent yes
    IdentityAgent /run/user/1000/ssh-agent.socket

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
    AddKeysToAgent yes
    IdentityAgent /run/user/1000/ssh-agent.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Normalizing the socket in bash and zsh
&lt;/h4&gt;

&lt;p&gt;Added to both &lt;code&gt;.bashrc&lt;/code&gt; and &lt;code&gt;.zshrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# OpenSSH ssh-agent (shared between .bashrc and .zshrc)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;/ssh-agent.socket"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;/ssh-agent.socket"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Dealing with Flatpak
&lt;/h3&gt;

&lt;p&gt;Flatpak's Obsidian runs inside its own sandbox. The host socket needs to be explicitly exposed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flatpak override &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filesystem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xdg-run/ssh-agent.socket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/user/1000/ssh-agent.socket &lt;span class="se"&gt;\&lt;/span&gt;
  md.obsidian.Obsidian

&lt;span class="c"&gt;# Remove gcr access&lt;/span&gt;
flatpak override &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nofilesystem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xdg-run/gcr &lt;span class="se"&gt;\&lt;/span&gt;
  md.obsidian.Obsidian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Obsidian Git wrapper
&lt;/h4&gt;

&lt;p&gt;Since the Obsidian Git plugin uses &lt;code&gt;/app/bin/git&lt;/code&gt; inside the Flatpak, I wrapped it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.local/bin/obsidian-git &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/usr/bin/env bash

SOCK="/run/user/1000/ssh-agent.socket"

export SSH_AUTH_SOCK="&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;"
export GIT_SSH_COMMAND="ssh &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -F /home/rbcn2000/.ssh/config &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -o IdentityAgent=&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -o BatchMode=yes &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -o ConnectTimeout=10 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -o ServerAliveInterval=5 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  -o ServerAliveCountMax=1"

exec /app/bin/git "&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/obsidian-git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BatchMode=yes&lt;/code&gt; is the critical part — it prevents the automated commit/sync process from silently hanging while waiting for a passphrase prompt that can never appear.&lt;/p&gt;

&lt;p&gt;In the Obsidian Git plugin settings, set &lt;code&gt;Custom Git binary path&lt;/code&gt; to &lt;code&gt;/home/rbcn2000/.local/bin/obsidian-git&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Automating Key Loading After Reboot
&lt;/h3&gt;

&lt;p&gt;Unlike gcr, OpenSSH ssh-agent does not restore keys automatically. After a reboot, the agent starts empty.&lt;/p&gt;

&lt;p&gt;I considered bringing keychain back, but decided against it — it risks creating duplicate agents. Instead, I wrote a minimal script whose only job is loading the keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.local/bin/ssh-load-github &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/usr/bin/env bash

set -u

SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;"

if [ ! -S "&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  systemctl --user start ssh-agent.service 2&amp;gt;/dev/null || true
fi

if [ ! -S "&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  echo "ssh-agent socket not found: &lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;amp;2
  exit 1
fi

# Don't wait for passphrase input in non-interactive environments
if [ ! -t 0 ]; then
  exit 0
fi

CURRENT_KEYS="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"

if ! printf '%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;' "&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_KEYS&lt;/span&gt;&lt;span class="sh"&gt;" | grep -q 'personal-github'; then
  ssh-add "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.ssh/id_ed25519_personal" || exit 1
fi

CURRENT_KEYS="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"

if ! printf '%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;' "&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_KEYS&lt;/span&gt;&lt;span class="sh"&gt;" | grep -q 'work-github'; then
  ssh-add "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.ssh/id_ed25519_work" || exit 1
fi
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/ssh-load-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it from &lt;code&gt;.zshrc&lt;/code&gt; and &lt;code&gt;.bashrc&lt;/code&gt; on interactive shell startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$-&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;i&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_LOAD_GITHUB_ON_SHELL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin/ssh-load-github"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the first time a terminal opens after reboot, you enter the passphrase once — and that's it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The .desktop Launcher Trap
&lt;/h3&gt;

&lt;p&gt;I thought it was solved. There was one more problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After a reboot, launching Obsidian first meant Git didn't work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The culprit was the TTY check inside &lt;code&gt;ssh-load-github&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0   &lt;span class="c"&gt;# No TTY → do nothing&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Obsidian launches from a &lt;code&gt;.desktop&lt;/code&gt; file, there is no TTY. So &lt;code&gt;ssh-load-github&lt;/code&gt; exits immediately, keys never get loaded, and Obsidian starts up with an empty agent.&lt;/p&gt;

&lt;h4&gt;
  
  
  The fix: SSH_ASKPASS for a GUI dialog
&lt;/h4&gt;

&lt;p&gt;OpenSSH has a built-in mechanism for exactly this situation: &lt;code&gt;SSH_ASKPASS&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ssh-askpass-gnome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a dedicated launcher that uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.local/bin/open-obsidian &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/usr/bin/env bash

SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;"

if [ ! -S "&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  systemctl --user start ssh-agent.service 2&amp;gt;/dev/null || true
fi

if [ ! -S "&lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  echo "ssh-agent socket not found: &lt;/span&gt;&lt;span class="nv"&gt;$SOCK&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;amp;2
  exit 1
fi

# Accept passphrase input via GTK dialog even without a TTY
export SSH_ASKPASS="/usr/lib/openssh/gnome-ssh-askpass"
export SSH_ASKPASS_REQUIRE=force

CURRENT_KEYS="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"

if ! printf '%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;' "&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_KEYS&lt;/span&gt;&lt;span class="sh"&gt;" | grep -q 'personal-github'; then
  ssh-add "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.ssh/id_ed25519_personal" || exit 1
fi

CURRENT_KEYS="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"

if ! printf '%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;' "&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_KEYS&lt;/span&gt;&lt;span class="sh"&gt;" | grep -q 'work-github'; then
  ssh-add "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.ssh/id_ed25519_work" || exit 1
fi

exec flatpak run md.obsidian.Obsidian "&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/open-obsidian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Overriding the .desktop file
&lt;/h4&gt;

&lt;p&gt;The system-side Flatpak &lt;code&gt;.desktop&lt;/code&gt; file can't be edited directly — Flatpak updates would overwrite it. Instead, copy it to the user applications directory and redirect the &lt;code&gt;Exec&lt;/code&gt; line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /var/lib/flatpak/exports/share/applications/md.obsidian.Obsidian.desktop &lt;span class="se"&gt;\&lt;/span&gt;
   ~/.local/share/applications/md.obsidian.Obsidian.desktop

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s|^Exec=.*|Exec=/home/rbcn2000/.local/bin/open-obsidian %U|'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   ~/.local/share/applications/md.obsidian.Obsidian.desktop

update-desktop-database ~/.local/share/applications/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, launching Obsidian from the app launcher shows a GTK passphrase dialog first, then starts Obsidian.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Final Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemd user
  ↓
OpenSSH ssh-agent.service
  ↓
/run/user/1000/ssh-agent.socket
  ├─ bash / zsh (ssh-load-github on shell startup)
  ├─ ~/.ssh/config (IdentityAgent explicit)
  └─ ~/.local/bin/open-obsidian  ← launched from .desktop
       └─ SSH_ASKPASS=gnome-ssh-askpass (GTK dialog)
            ↓
          Flatpak Obsidian
               ↓
             ~/.local/bin/obsidian-git (BatchMode=yes)
               ↓
             /app/bin/git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a reboot, behavior is now fully symmetric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Opening a terminal first:
  → ssh-load-github runs
  → passphrase entered once
  → shared across bash / zsh / Obsidian for the rest of the session

Opening Obsidian first:
  → open-obsidian runs
  → GTK dialog prompts for passphrase
  → shared across bash / zsh / Obsidian for the rest of the session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Separation of Concerns
&lt;/h3&gt;

&lt;p&gt;In the end, the responsibilities around ssh-agent sorted themselves into a clear table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Handled by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent startup&lt;/td&gt;
&lt;td&gt;systemd user &lt;code&gt;ssh-agent.service&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent selection&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; / &lt;code&gt;IdentityAgent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key loading (terminal)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ssh-load-github&lt;/code&gt; (TTY present)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key loading (GUI launch)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;open-obsidian&lt;/code&gt; + &lt;code&gt;SSH_ASKPASS&lt;/code&gt; (no TTY)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unattended Git operations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BatchMode=yes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tools like keychain that handle everything in one shot are convenient, but they make it easy to accidentally create duplicate agents or fail to reach GUI applications. Separating the responsibilities made it clear exactly what was happening at every step.&lt;/p&gt;




&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;An environment variable is not a "setting" — it is the result of process inheritance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even if &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; is correctly set somewhere, it only reaches a child process if that child is spawned from a process that has it. Configuration written in &lt;code&gt;.zshrc&lt;/code&gt; simply does not reach GUI applications. That was the root of everything.&lt;/p&gt;

&lt;p&gt;And an additional lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sometimes a fixed socket with a simple agent is more deterministic than a polished GUI integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;gcr-ssh-agent's desktop integration is elegant. But in this environment, its auto-restore mechanism turned out to be unstable at signing time. A plain OpenSSH ssh-agent on a fixed socket was far more predictable.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ssh</category>
      <category>git</category>
      <category>flatpak</category>
    </item>
    <item>
      <title>I Quit .bashrc.</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Sat, 30 May 2026 17:47:33 +0000</pubDate>
      <link>https://dev.to/rbcn/i-quit-bashrc-1d21</link>
      <guid>https://dev.to/rbcn/i-quit-bashrc-1d21</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;One morning, the &lt;code&gt;j&lt;/code&gt; command was gone.&lt;/p&gt;

&lt;p&gt;I had been using it as an alias for zoxide, but suddenly it returned &lt;code&gt;command not found&lt;/code&gt;. It worked fine in Ghostty, but not in VSCode's integrated terminal. Zed behaved differently yet again.&lt;/p&gt;

&lt;p&gt;I had built the perfect &lt;em&gt;"I can't explain why this works"&lt;/em&gt; environment.&lt;/p&gt;

&lt;p&gt;This is the story of what happens when you keep piling settings into &lt;code&gt;.bashrc&lt;/code&gt; — and what you gain when you stop.&lt;/p&gt;




&lt;h3&gt;
  
  
  Before: The Everything-in-.bashrc Era
&lt;/h3&gt;

&lt;p&gt;When I started Linux development, I added a new line to &lt;code&gt;.bashrc&lt;/code&gt; every time I introduced a useful tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .bashrc (bloated state)&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;starship init bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;zoxide init bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fzf &lt;span class="nt"&gt;--bash&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--eval&lt;/span&gt; &lt;span class="nt"&gt;--agents&lt;/span&gt; ssh id_ed25519_work id_ed25519_personal&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bash_aliases
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# ... secrets, completions, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliases, completions, environment variables, and tool initializations were all jammed into a single file.&lt;/p&gt;

&lt;p&gt;This caused three main problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① PATH inconsistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some tools would have their paths registered and some wouldn't. Every time I opened a new terminal, things might behave differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② Behavioral differences across terminals&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Commands that worked in Ghostty didn't work in VSCode's integrated terminal. The disappearing &lt;code&gt;j&lt;/code&gt; command was the clearest symptom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ Loss of reproducibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I could no longer explain &lt;em&gt;why&lt;/em&gt; things worked. Was it my config? Something I ran earlier? I had no idea.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Insight: Shell ≠ Environment
&lt;/h3&gt;

&lt;p&gt;As I worked through the problem, something clicked.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shell ≠ Environment&lt;br&gt;
Shell = UI (the interface)&lt;br&gt;
Environment = execution context&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;.bashrc&lt;/code&gt; had been doing two completely different jobs at once: configuring the OS-standard shell &lt;em&gt;and&lt;/em&gt; initializing the development environment. That was the root of all the confusion.&lt;/p&gt;

&lt;p&gt;The problem wasn't the tools themselves (zoxide, starship). It was the &lt;strong&gt;mixing of initialization paths&lt;/strong&gt;. Login shells and interactive shells read different files — that discrepancy was causing the behavioral differences across terminals.&lt;/p&gt;




&lt;h3&gt;
  
  
  After: Decomposing into Three Layers
&lt;/h3&gt;

&lt;p&gt;Based on this insight, I split the environment into three distinct layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash     = OS-standard / preservation layer
zsh      = development environment layer
terminal = display layer (all unified to zsh -l)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Returning bash to a preservation layer
&lt;/h4&gt;

&lt;p&gt;I reset &lt;code&gt;.bashrc&lt;/code&gt; to the system default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /etc/skel/.bashrc ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing I kept was an expanded &lt;code&gt;HISTSIZE&lt;/code&gt;. All development-specific configuration was removed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Consolidating the development environment into zsh
&lt;/h4&gt;

&lt;p&gt;Development tool initialization moved to &lt;code&gt;.zshrc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.zshrc&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;starship init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;zoxide init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zsh_aliases
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aliases were also separated into &lt;code&gt;.zsh_aliases&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Unifying all terminals to &lt;code&gt;zsh -l&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;I updated each terminal's configuration to launch zsh as a login shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ghostty&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command = /usr/bin/zsh -l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;VSCode&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"terminal.integrated.profiles.linux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"zsh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/usr/bin/zsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-l"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"terminal.integrated.defaultProfile.linux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zsh"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zed (v1.0.0)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"terminal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"with_arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"program"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/usr/bin/zsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-l"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WezTerm (&lt;code&gt;~/.wezterm.lua&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;wezterm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'wezterm'&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;default_prog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'/usr/bin/zsh'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-l'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wezterm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'JetBrainsMono Nerd Font'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;font_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;color_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Dracula"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enable_tab_bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WezTerm takes the shell and its arguments as an array via &lt;code&gt;default_prog&lt;/code&gt;. Font settings can be unified here as well.&lt;/p&gt;




&lt;h3&gt;
  
  
  Gotchas Along the Way
&lt;/h3&gt;

&lt;p&gt;A few things tripped me up during the migration.&lt;/p&gt;

&lt;h4&gt;
  
  
  Icons turn into tofu squares in VSCode only
&lt;/h4&gt;

&lt;p&gt;Symptoms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ghostty → OK
Zed     → OK
VSCode  → icons render as □ (tofu)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause: VSCode has a separate font setting for its integrated terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"terminal.integrated.fontFamily"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JetBrainsMono Nerd Font Mono"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding this line fixed it.&lt;/p&gt;

&lt;h4&gt;
  
  
  The display name and actual name of the font don't match
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Display name: JetBrainsMono Nerd Font Mono
Actual name:  JetBrainsMono NFM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ghostty resolves this transparently via fontconfig. WezTerm and Zed may require specifying &lt;code&gt;JetBrainsMono NFM&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;j&lt;/code&gt; command disappeared
&lt;/h4&gt;

&lt;p&gt;I had &lt;code&gt;alias j='z'&lt;/code&gt; in &lt;code&gt;.bash_aliases&lt;/code&gt;, but after migrating to zsh it needed to move to &lt;code&gt;.zsh_aliases&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.zsh_aliases&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;j&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'z'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ji&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'zi'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;code&gt;$0&lt;/code&gt; and &lt;code&gt;$SHELL&lt;/code&gt; show different values
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;      &lt;span class="c"&gt;# → zsh&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SHELL&lt;/span&gt;  &lt;span class="c"&gt;# → /bin/bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$SHELL&lt;/code&gt; reflects the login shell, not necessarily the currently running shell. Adding this to &lt;code&gt;.zshrc&lt;/code&gt; aligns them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/zsh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: everything in .bashrc → non-deterministic environment
After:  bash=preservation / zsh=development / terminal=display → determinism restored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Same environment regardless of which terminal launches it&lt;/li&gt;
&lt;li&gt;PATH issues eliminated&lt;/li&gt;
&lt;li&gt;Debugging scope narrowed to the application itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest win: I can now explain &lt;em&gt;why&lt;/em&gt; things work.&lt;/p&gt;




&lt;h3&gt;
  
  
  Closing
&lt;/h3&gt;

&lt;p&gt;Quitting &lt;code&gt;.bashrc&lt;/code&gt; wasn't about abandoning tools.&lt;br&gt;
It was about &lt;strong&gt;making ownership of the environment explicit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Shell is UI. Environment is execution context. Cramming both into the same file was the source of all the confusion.&lt;/p&gt;

&lt;p&gt;In the next post, I'll cover what happened immediately after this migration: Obsidian Git's SSH authentication broke, and I found myself in a battle with ssh-agent and the Flatpak sandbox.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>shell</category>
      <category>bash</category>
      <category>zsh</category>
    </item>
  </channel>
</rss>
