<?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.us-east-2.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>Frugal Architect for SIers: Reflections from AWS GameDay</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Sun, 28 Jun 2026 15:00:45 +0000</pubDate>
      <link>https://dev.to/rbcn/frugal-architect-for-siers-reflections-from-aws-gameday-2lbe</link>
      <guid>https://dev.to/rbcn/frugal-architect-for-siers-reflections-from-aws-gameday-2lbe</guid>
      <description>&lt;h3&gt;
  
  
  I enjoyed GameDay this year
&lt;/h3&gt;

&lt;p&gt;AWS Summit Tokyo 2026 was held over two days starting on June 25, 2026.&lt;br&gt;
I joined the GameDay on Day 01. It was my first GameDay.&lt;br&gt;
I also want to thank the people I briefly teamed up with that day.&lt;/p&gt;

&lt;p&gt;The theme was Frugal Architect: the idea that, in cloud architecture, cost should not be treated as something to optimize after the fact, but as a constraint and quality attribute to be considered from the beginning of design.&lt;/p&gt;

&lt;h3&gt;
  
  
  This is not a GameDay walkthrough
&lt;/h3&gt;

&lt;p&gt;Let me be clear up front: this article is not a GameDay walkthrough.&lt;/p&gt;

&lt;p&gt;I will not write about the specific quests, completion conditions, resource configurations, settings, solution steps, pitfalls, scoring, or tactical observations. That would go against the spirit of the participation terms and would also spoil the experience for future participants.&lt;/p&gt;

&lt;p&gt;What I want to write about instead is how GameDay helped me put Frugal Architect into the language of SIers—systems integrators.&lt;/p&gt;

&lt;p&gt;In short, Frugal Architect was not a collection of cloud cost-saving tricks.&lt;/p&gt;

&lt;p&gt;It was a vocabulary for reconnecting things that often become separated in SI projects: architecture, estimation, operations, and continuous improvement.&lt;/p&gt;




&lt;h3&gt;
  
  
  Frugal Architect is not merely a “cheap architect”
&lt;/h3&gt;

&lt;p&gt;The word “frugal” can easily sound like “cheap” or “cost-cutting.”&lt;/p&gt;

&lt;p&gt;But a Frugal Architect is not simply someone who chooses the cheapest possible configuration.&lt;/p&gt;

&lt;p&gt;Anyone can make something cheaper in the short term. Remove redundancy. Reduce monitoring. Reduce logs. Remove performance headroom. Let humans compensate through operations. The monthly bill might go down.&lt;/p&gt;

&lt;p&gt;But that is not architecture. That is just endurance.&lt;/p&gt;

&lt;p&gt;What Frugal Architect deals with, I think, is the relationship between cost and value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which business value does this cost support?&lt;/li&gt;
&lt;li&gt;Which risk does this redundancy reduce?&lt;/li&gt;
&lt;li&gt;Which decision does this monitoring make possible?&lt;/li&gt;
&lt;li&gt;Which user experience does this performance headroom protect?&lt;/li&gt;
&lt;li&gt;Whose time does this operational burden consume?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the point is not to erase spending, but to make spending accountable.&lt;/p&gt;

&lt;p&gt;In SIer language, this is the practice of connecting cost to design rationale.&lt;/p&gt;




&lt;h3&gt;
  
  
  In SI projects, cost is often placed outside architecture
&lt;/h3&gt;

&lt;p&gt;In SI projects, the word “cost” appears constantly.&lt;/p&gt;

&lt;p&gt;Estimates, budgets, approvals, contracts, effort, maintenance fees, cloud usage fees. Cost is everywhere.&lt;/p&gt;

&lt;p&gt;And yet, when it comes to architecture design, cost is not always treated as a real design requirement.&lt;/p&gt;

&lt;p&gt;Performance requirements are written down.&lt;/p&gt;

&lt;p&gt;Availability requirements are written down.&lt;/p&gt;

&lt;p&gt;Security requirements are written down.&lt;/p&gt;

&lt;p&gt;Backup requirements are written down.&lt;/p&gt;

&lt;p&gt;But cost requirements often stop at vague phrases such as “keep it as low as possible,” “stay within budget,” or “be careful with pay-as-you-go pricing.”&lt;/p&gt;

&lt;p&gt;That is not enough to support design decisions.&lt;/p&gt;

&lt;p&gt;Just as “make it as fast as possible” is not a performance requirement, “make it as cheap as possible” is not a cost requirement.&lt;/p&gt;

&lt;p&gt;At minimum, cost needs to be framed in ways that architecture can actually use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected monthly run cost under normal conditions&lt;/li&gt;
&lt;li&gt;which cost items increase as usage grows, and how&lt;/li&gt;
&lt;li&gt;under what conditions cost increases are acceptable&lt;/li&gt;
&lt;li&gt;what can and cannot be sacrificed to reduce cost&lt;/li&gt;
&lt;li&gt;why we pay for monitoring, logs, backups, and redundancy&lt;/li&gt;
&lt;li&gt;when and by whom cost will be reviewed after release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not merely a cloud billing topic.&lt;/p&gt;

&lt;p&gt;It is a non-functional requirement that makes design decisions explainable later.&lt;/p&gt;




&lt;h3&gt;
  
  
  Separating initial cost and run cost is not enough
&lt;/h3&gt;

&lt;p&gt;In SI estimates, it is common to separate initial build cost from operation and maintenance cost.&lt;/p&gt;

&lt;p&gt;That distinction is necessary.&lt;/p&gt;

&lt;p&gt;But cloud architecture requires one more step.&lt;/p&gt;

&lt;p&gt;Cloud cost is not something fixed at build time. It continues to change with usage patterns, data volume, access patterns, log volume, backup retention, and the granularity of operational monitoring.&lt;/p&gt;

&lt;p&gt;In other words, cloud cost is not “an amount decided during estimation.” It is a variable to be observed and adjusted during operations.&lt;/p&gt;

&lt;p&gt;If we treat it like a fixed cost, two types of failure occur.&lt;/p&gt;

&lt;p&gt;The first is making the initial design excessively conservative.&lt;/p&gt;

&lt;p&gt;The second is allowing something that looked cheap at first to quietly grow during operations.&lt;/p&gt;

&lt;p&gt;Both are familiar in SI work.&lt;/p&gt;

&lt;p&gt;The former is called “designing on the safe side.” The latter is called “unexpected usage growth.”&lt;/p&gt;

&lt;p&gt;From the Frugal Architect perspective, both are failures of observation and control.&lt;/p&gt;




&lt;h3&gt;
  
  
  FinOps is the higher-level concept; Frugal Architect is the implementation language
&lt;/h3&gt;

&lt;p&gt;On reflection, I think FinOps is a higher-level abstraction than Frugal Architect.&lt;/p&gt;

&lt;p&gt;FinOps is a management discipline for continuously aligning cloud usage, cost, and value across technology, business, finance, and operations. Cost visibility, tagging, accountability, budget management, usage analysis, and organizational decision-making all belong clearly to the FinOps domain.&lt;/p&gt;

&lt;p&gt;Frugal Architect, on the other hand, feels closer to an implementation language for bringing that thinking into architecture.&lt;/p&gt;

&lt;p&gt;If FinOps is the operating philosophy for making cloud usage governable, Frugal Architect is the engineer’s language for turning that philosophy into design decisions.&lt;/p&gt;

&lt;p&gt;Which components should be synchronous?&lt;/p&gt;

&lt;p&gt;Where should processing become asynchronous?&lt;/p&gt;

&lt;p&gt;Where should caching be placed?&lt;/p&gt;

&lt;p&gt;How much should be delegated to managed services?&lt;/p&gt;

&lt;p&gt;Which logs need to be immediately searchable, and which can be retained more cheaply for later use?&lt;/p&gt;

&lt;p&gt;Which risks are worth paying for, and which risks should be accepted operationally?&lt;/p&gt;

&lt;p&gt;These decisions are often too late if they begin only after the invoice arrives.&lt;/p&gt;

&lt;p&gt;At the same time, in many organizations, FinOps still seems to remain at the level of slogans and calls to action.&lt;/p&gt;

&lt;p&gt;“Let’s optimize cloud cost.”&lt;/p&gt;

&lt;p&gt;“Let’s make each department cost-aware.”&lt;/p&gt;

&lt;p&gt;“Let’s add tags.”&lt;/p&gt;

&lt;p&gt;None of these are wrong. But unless they connect to actual design decisions, FinOps ends as a slogan.&lt;/p&gt;

&lt;p&gt;In SIer language, Frugal Architect is architecture design that includes operating cost. It is also non-functional design for bringing FinOps down into design, build, and maintenance work.&lt;/p&gt;




&lt;h3&gt;
  
  
  Cost is a non-functional requirement
&lt;/h3&gt;

&lt;p&gt;I understand the first message of Frugal Architect as this: cost must be treated as a non-functional requirement.&lt;/p&gt;

&lt;p&gt;For SIers, that is an important translation.&lt;/p&gt;

&lt;p&gt;Once cost enters the category of non-functional requirements, it is no longer merely a topic for procurement, contracts, or billing.&lt;/p&gt;

&lt;p&gt;It becomes something that can be handled in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requirements definition&lt;/li&gt;
&lt;li&gt;design reviews&lt;/li&gt;
&lt;li&gt;test perspectives&lt;/li&gt;
&lt;li&gt;operations design&lt;/li&gt;
&lt;li&gt;acceptance criteria&lt;/li&gt;
&lt;li&gt;improvement cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simply saying “cost matters” is weak.&lt;/p&gt;

&lt;p&gt;Saying “cost is a non-functional requirement” gives it a place in design artifacts.&lt;/p&gt;

&lt;p&gt;For example, we can bring questions like these into non-functional requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the unit cost of this system?&lt;/li&gt;
&lt;li&gt;If usage doubles, does cost also double, or does it grow faster?&lt;/li&gt;
&lt;li&gt;Who detects cost anomalies, how often, and with which metrics?&lt;/li&gt;
&lt;li&gt;Which SLOs must be preserved when reducing cost?&lt;/li&gt;
&lt;li&gt;Does the system need the same configuration during normal and peak periods?&lt;/li&gt;
&lt;li&gt;For audit, investigation, and incident response, which data must be retained and at what granularity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions cannot be answered by cloud architects alone.&lt;/p&gt;

&lt;p&gt;They must be decided together with business teams, operations teams, security teams, and contract stakeholders.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of consensus-building that SIers are supposed to be good at.&lt;/p&gt;




&lt;h3&gt;
  
  
  Not “expensive or cheap,” but “explainable or not”
&lt;/h3&gt;

&lt;p&gt;In SI work, cost reviews often become discussions about whether something is expensive or cheap.&lt;/p&gt;

&lt;p&gt;Of course, budgets matter.&lt;/p&gt;

&lt;p&gt;But I think the more essential question is whether the cost can be explained.&lt;/p&gt;

&lt;p&gt;Some costs are high but explainable.&lt;/p&gt;

&lt;p&gt;Some cost reductions are cheap but dangerous.&lt;/p&gt;

&lt;p&gt;For example, if a redundant architecture adds cost but reduces business continuity risk, that cost can be explained. On the other hand, if unused resources or purposeless log retention continue to generate cost, even small amounts become hard to justify.&lt;/p&gt;

&lt;p&gt;A Frugal Architect-style design review asks questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which risk does this cost reduce?&lt;/li&gt;
&lt;li&gt;Which customer experience does this cost protect?&lt;/li&gt;
&lt;li&gt;Which operational task does this cost reduce?&lt;/li&gt;
&lt;li&gt;Does this cost contribute to future changeability?&lt;/li&gt;
&lt;li&gt;If this cost increases, can we trace why it increased?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The starting point for cloud cost design is not the amount itself, but whether the causal relationship can be traced.&lt;/p&gt;




&lt;h3&gt;
  
  
  Is “we cannot let the system go down” becoming an excuse for overspecification?
&lt;/h3&gt;

&lt;p&gt;There is another question SIers cannot avoid.&lt;/p&gt;

&lt;p&gt;Is the phrase “we cannot let the system go down” becoming an excuse to justify overspecification?&lt;/p&gt;

&lt;p&gt;Of course, some systems really must not go down.&lt;/p&gt;

&lt;p&gt;Social infrastructure, payments, healthcare, security, core business systems, customer-facing systems. If downtime would have major business or social consequences, investing in availability is absolutely reasonable.&lt;/p&gt;

&lt;p&gt;The problem begins when “must not go down” remains undifferentiated and every component is given the same level of strength.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which functions would truly cause business impact if they stopped?&lt;/li&gt;
&lt;li&gt;At what time of day would downtime be most harmful?&lt;/li&gt;
&lt;li&gt;How many minutes of downtime are tolerable?&lt;/li&gt;
&lt;li&gt;Is degraded operation or manual fallback possible?&lt;/li&gt;
&lt;li&gt;Would read-only operation be enough?&lt;/li&gt;
&lt;li&gt;Which matters more: recovery time or data loss?&lt;/li&gt;
&lt;li&gt;Does every function need the same availability level?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these questions are not broken down, the design naturally shifts toward the safe side.&lt;/p&gt;

&lt;p&gt;Designing on the safe side is not evil. But the cost is transferred somewhere.&lt;/p&gt;

&lt;p&gt;It appears as infrastructure cost. It appears as operation cost. It appears as maintenance cost. Eventually, it appears in service pricing or customer burden.&lt;/p&gt;

&lt;p&gt;In other words, unexplained availability leads to unexplained price increases.&lt;/p&gt;

&lt;p&gt;What Frugal Architect asks is not “do not make systems highly available.”&lt;/p&gt;

&lt;p&gt;It asks:&lt;/p&gt;

&lt;p&gt;“What business value does this availability protect?”&lt;/p&gt;

&lt;p&gt;“How much is the business, or the user, willing to pay for it?”&lt;/p&gt;

&lt;p&gt;“Can the same purpose be met through a cheaper architecture, operation model, or graceful degradation design?”&lt;/p&gt;

&lt;p&gt;Availability is not sacred. It is a non-functional requirement, and therefore a trade-off.&lt;/p&gt;

&lt;p&gt;That is why we need to distinguish between availability that must not be cut and availability that has simply been overbuilt.&lt;/p&gt;




&lt;h3&gt;
  
  
  It takes courage to cut, and courage not to cut
&lt;/h3&gt;

&lt;p&gt;Observability is important.&lt;/p&gt;

&lt;p&gt;But before observability, we need the attitude to decide what to cut and what not to cut.&lt;/p&gt;

&lt;p&gt;It takes courage to cut.&lt;/p&gt;

&lt;p&gt;Review a configuration that has been running for a long time. Reduce resources that had extra headroom. Stop excessive retention or excessive redundancy. Delete things that are no longer used. These are not merely cost-reduction tasks. They are acts of questioning things that had quietly become sources of reassurance: “Do we really need this?”&lt;/p&gt;

&lt;p&gt;It also takes courage not to cut.&lt;/p&gt;

&lt;p&gt;When a cost-reduction mandate becomes strong, it is tempting to cut whatever produces visible short-term savings: monitoring, logs, backups, redundancy, security, test environments. But if those things support incident response, auditability, recovery, or customer trust, then the decision not to cut them also needs accountability.&lt;/p&gt;

&lt;p&gt;In other words, a Frugal Architect is not “someone who cuts.”&lt;/p&gt;

&lt;p&gt;A Frugal Architect cuts what should be cut and keeps what should be kept.&lt;/p&gt;

&lt;p&gt;Observability is needed to support that judgment.&lt;/p&gt;

&lt;p&gt;It is not that we cannot cut because we cannot see. Cutting while blind is dangerous.&lt;/p&gt;

&lt;p&gt;It is not that we should keep things because we cannot see. Keeping things while blind preserves overspecification.&lt;/p&gt;

&lt;p&gt;Observability is not the goal. It is the prerequisite that supports the courage to cut and the courage not to cut.&lt;/p&gt;




&lt;h3&gt;
  
  
  Practice means making small improvements repeatable
&lt;/h3&gt;

&lt;p&gt;One thing GameDay made very clear to me was that cost optimization is never a one-shot activity.&lt;/p&gt;

&lt;p&gt;We should not try to guess the perfect architecture from the beginning.&lt;/p&gt;

&lt;p&gt;Observe. Form a hypothesis. Make a small change. Watch the impact. Move to the next improvement.&lt;/p&gt;

&lt;p&gt;That loop matters.&lt;/p&gt;

&lt;p&gt;In SI projects, the center of gravity changes after release.&lt;/p&gt;

&lt;p&gt;The build team leaves, and the operations and maintenance team takes over. The original design intent fades. Even when there is room for improvement, the system can fall into a state of “it is stable, so do not touch it.”&lt;/p&gt;

&lt;p&gt;But in the cloud, “running stably” and “running at an appropriate cost” are different things.&lt;/p&gt;

&lt;p&gt;This is where operations and maintenance can create new value.&lt;/p&gt;

&lt;p&gt;Not merely preventing incidents, but continuously improving the cost efficiency of systems that keep running.&lt;/p&gt;

&lt;p&gt;This can be incorporated into monthly reports and regular meetings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;major cost changes from the previous month&lt;/li&gt;
&lt;li&gt;relationship between usage and cost&lt;/li&gt;
&lt;li&gt;review of unused or underused resources&lt;/li&gt;
&lt;li&gt;validity of log, backup, and data retention policies&lt;/li&gt;
&lt;li&gt;small optimization experiments for the next month&lt;/li&gt;
&lt;li&gt;costs that were intentionally not reduced, and why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these items become part of regular operational reporting, maintenance changes from “keeping the system from stopping” to “improving cost per value.”&lt;/p&gt;




&lt;h3&gt;
  
  
  Practices SIers can take back to real projects
&lt;/h3&gt;

&lt;p&gt;Without touching specific GameDay content, I think SIers can bring the following practices back to real projects.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Add a cost section to non-functional requirements
&lt;/h4&gt;

&lt;p&gt;Cost should not live only in a separate estimate sheet. It should be part of non-functional requirements.&lt;/p&gt;

&lt;p&gt;What needs to be written is not only the amount.&lt;/p&gt;

&lt;p&gt;Write the unit cost, growth factors, monitoring method, qualities that must be preserved during cost reduction, and review frequency.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Record cost rationale in architecture decision records
&lt;/h4&gt;

&lt;p&gt;In ADRs, record not only performance, availability, and security rationale, but also cost rationale.&lt;/p&gt;

&lt;p&gt;If we leave behind why this architecture was chosen, why this headroom was included, or why we intentionally did not optimize something yet, we can revisit those decisions later.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Treat tagging as accountability design, not billing management
&lt;/h4&gt;

&lt;p&gt;Tags are not only for splitting invoices.&lt;/p&gt;

&lt;p&gt;They are a design for accountability: who will look at this, who will fix it, and which service, environment, or role it belongs to.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Do not decide logging and monitoring by “more is safer”
&lt;/h4&gt;

&lt;p&gt;More logs and metrics are not automatically better.&lt;/p&gt;

&lt;p&gt;Define the required visibility and retention period. Separate what must be queryable immediately from what only needs to be available later.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Include continuous cost improvement in operations and maintenance
&lt;/h4&gt;

&lt;p&gt;Do not treat cloud cost review as a special task after an incident or budget problem.&lt;/p&gt;

&lt;p&gt;Make it part of monthly or quarterly operations.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Make both “costs to cut” and “costs not to cut” explicit
&lt;/h4&gt;

&lt;p&gt;Cost optimization is not only about deciding what to cut. It is also about deciding what not to cut.&lt;/p&gt;

&lt;p&gt;It takes courage to cut, and courage not to cut.&lt;/p&gt;

&lt;p&gt;Costs that support security, auditability, availability, recoverability, and other business-critical qualities should be explained as investments, not treated as automatic reduction targets.&lt;/p&gt;




&lt;h3&gt;
  
  
  My biggest takeaway
&lt;/h3&gt;

&lt;p&gt;My biggest takeaway was that Frugal Architect is not merely a set of optimization techniques for cloud engineers. It is a language of design responsibility.&lt;/p&gt;

&lt;p&gt;SIers stand in a position where they must handle the customer’s business, budget, risk, operations model, contracts, and future changes together.&lt;/p&gt;

&lt;p&gt;In that sense, Frugal Architect fits SI work very well.&lt;/p&gt;

&lt;p&gt;But if we reduce it to “we can lower your AWS cost,” it becomes shallow.&lt;/p&gt;

&lt;p&gt;The real value lies in being able to discuss these questions with the customer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much are we willing to pay for each quality attribute?&lt;/li&gt;
&lt;li&gt;How much are we willing to pay for each risk reduction?&lt;/li&gt;
&lt;li&gt;How should we prepare for future growth?&lt;/li&gt;
&lt;li&gt;Which costs should we reduce now?&lt;/li&gt;
&lt;li&gt;Which costs should we keep for future flexibility?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Making these questions a shared language across design, estimation, and operations is, to me, the SIer-style practice of Frugal Architect.&lt;/p&gt;




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

&lt;p&gt;AWS GameDay was a place where I could experience architectural thinking by actually doing things with my hands.&lt;/p&gt;

&lt;p&gt;But its value was not in learning “what to configure and how.” It was in physically understanding why certain decisions matter.&lt;/p&gt;

&lt;p&gt;Treat cost as a non-functional requirement.&lt;/p&gt;

&lt;p&gt;Connect cost to business value.&lt;/p&gt;

&lt;p&gt;Observe, explain, and continuously adjust cost.&lt;/p&gt;

&lt;p&gt;This is not a bag of cloud tricks. It is a design attitude that SI work will increasingly need.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>frugalarchitect</category>
      <category>sier</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>Constraints Are Interfaces: What the TrackPoint Teaches About Tool Design</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Fri, 19 Jun 2026 01:09:49 +0000</pubDate>
      <link>https://dev.to/rbcn/constraints-are-interfaces-what-the-trackpoint-teaches-about-tool-design-f2d</link>
      <guid>https://dev.to/rbcn/constraints-are-interfaces-what-the-trackpoint-teaches-about-tool-design-f2d</guid>
      <description>&lt;h3&gt;
  
  
  The ThinkPad That Was Supposed to Be a Side Machine
&lt;/h3&gt;

&lt;p&gt;I bought a cheap used laptop as a casual work machine.&lt;/p&gt;

&lt;p&gt;It was a ThinkPad X1 Carbon Gen 7. I installed Linux Mint and meant to use it as a secondary machine: light research, writing, a bit of work on the go. Nothing more than that.&lt;/p&gt;

&lt;p&gt;Then it turned out to be better than expected.&lt;/p&gt;

&lt;p&gt;Thin. Light. Good keyboard. Linux Mint ran without much drama. For a corporate lease-return laptop, it was not living out its retirement. It was still a perfectly capable working machine. Before I knew it, the side machine had become my main one.&lt;/p&gt;

&lt;p&gt;But one thing kept bothering me.&lt;/p&gt;

&lt;p&gt;The touchpad.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sometimes Convenience Gets in the Way
&lt;/h3&gt;

&lt;p&gt;The touchpad itself was not bad.&lt;/p&gt;

&lt;p&gt;If anything, it was normally useful. It scrolls. It handles fine movements. On a modern laptop, it is the expected pointing device.&lt;/p&gt;

&lt;p&gt;But in my own use, misfires started to stand out.&lt;/p&gt;

&lt;p&gt;While writing, my palm or the base of my thumb would touch it without me noticing. The cursor would jump. I would type into the wrong place. A selection would change unexpectedly.&lt;/p&gt;

&lt;p&gt;A small accident.&lt;/p&gt;

&lt;p&gt;But for a machine used mainly for writing, it mattered.&lt;/p&gt;

&lt;p&gt;Then I remembered something.&lt;/p&gt;

&lt;p&gt;My first laptop had also been a ThinkPad: an IBM ThinkPad R31. Back then, there was no giant modern touchpad experience. There was a red dot in the middle of the keyboard, and you moved the pointer by pushing it.&lt;/p&gt;

&lt;p&gt;So maybe I should simply go back.&lt;/p&gt;

&lt;p&gt;Disable the touchpad. Return to the TrackPoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  At First, It Was Just touchpad-off
&lt;/h3&gt;

&lt;p&gt;The first step was simple.&lt;/p&gt;

&lt;p&gt;I checked the touchpad name with &lt;code&gt;xinput list&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;SYNA8004:00 06CB:CD8B Touchpad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I disabled it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xinput disable &lt;span class="s2"&gt;"SYNA8004:00 06CB:CD8B Touchpad"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That alone changed the feel of the machine.&lt;/p&gt;

&lt;p&gt;My hands stayed near the home position. Pointer movement converged on the red dot. As a writing machine, the whole posture became more consistent.&lt;/p&gt;

&lt;p&gt;At first, I treated this as a simple toggle.&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;#!/usr/bin/env bash&lt;/span&gt;
xinput disable &lt;span class="s2"&gt;"SYNA8004:00 06CB:CD8B Touchpad"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if needed, turn it back on.&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;#!/usr/bin/env bash&lt;/span&gt;
xinput &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="s2"&gt;"SYNA8004:00 06CB:CD8B Touchpad"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the world of &lt;code&gt;touchpad-off.sh&lt;/code&gt; and &lt;code&gt;touchpad-on.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At that point, it was still just a convenience setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Colliding with an Old X11 UX
&lt;/h3&gt;

&lt;p&gt;But disabling the touchpad was not the end of it.&lt;/p&gt;

&lt;p&gt;To scroll with the TrackPoint, you hold the middle button and push the red dot. That gesture is very ThinkPad-like. But on Linux / X11, the middle button also has another meaning.&lt;/p&gt;

&lt;p&gt;Middle-click paste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select text
↓
it enters PRIMARY selection
↓
middle click
↓
paste
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an old X11 convenience. For people who know and use it, it is fast. But it was not part of the UX I had internalized when I first used a ThinkPad on Windows XP.&lt;/p&gt;

&lt;p&gt;For my body, the middle button was not a paste button.&lt;/p&gt;

&lt;p&gt;It was the button for TrackPoint scrolling.&lt;/p&gt;

&lt;p&gt;So accidents happened.&lt;/p&gt;

&lt;p&gt;I would be reading code or text. I would scroll. Some text I had selected somewhere would be pasted unexpectedly. Different from cursor jumps, but still another kind of input-path noise.&lt;/p&gt;

&lt;p&gt;I could not simply kill the middle button itself. I needed it for scrolling.&lt;/p&gt;

&lt;p&gt;The thing to kill was not the button, but its interpretation as paste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xfconf-query &lt;span class="nt"&gt;-c&lt;/span&gt; xsettings &lt;span class="nt"&gt;-p&lt;/span&gt; /Gtk/EnablePrimaryPaste &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; bool &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;false &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; xfconf-query &lt;span class="nt"&gt;-c&lt;/span&gt; xsettings &lt;span class="nt"&gt;-p&lt;/span&gt; /Gtk/EnablePrimaryPaste &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this only expressed a policy for GTK / Xfce applications. It did not remove middle-click paste from X11 as a whole. Electron apps, terminals, and some editors could still interpret Button2 on their own.&lt;/p&gt;

&lt;p&gt;So in the end, I stopped delivering Button2 from the TrackPoint device to applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xinput set-button-map &lt;span class="s2"&gt;"TPPS/2 Elan TrackPoint"&lt;/span&gt; 1 0 3 4 5 6 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;0&lt;/code&gt; means: do not send the middle button to applications. At the same time, libinput can still keep that physical button as the modifier for TrackPoint scrolling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xinput set-prop &lt;span class="s2"&gt;"TPPS/2 Elan TrackPoint"&lt;/span&gt; &lt;span class="s2"&gt;"libinput Scroll Method Enabled"&lt;/span&gt; 0 0 1
xinput set-prop &lt;span class="s2"&gt;"TPPS/2 Elan TrackPoint"&lt;/span&gt; &lt;span class="s2"&gt;"libinput Button Scrolling Button"&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the middle button remains for scrolling. Middle-click paste is sealed away. Paste is consolidated into Ctrl+V / Ctrl+Shift+V.&lt;/p&gt;

&lt;h3&gt;
  
  
  It Became Mode-In
&lt;/h3&gt;

&lt;p&gt;At this point, what I was doing was no longer just turning the touchpad on and off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Disable the touchpad
Disable PrimaryPaste
Fix the TrackPoint as the primary input device
Keep the middle button for scrolling
Stop delivering Button2 to applications
Move paste into explicit keyboard shortcuts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a transition into an input mode.&lt;/p&gt;

&lt;p&gt;So I renamed the script.&lt;/p&gt;

&lt;p&gt;Not &lt;code&gt;touchpad-off.sh&lt;/code&gt;, but &lt;code&gt;thinkpadder-mode-in.sh&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;TOUCHPAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SYNA8004:00 06CB:CD8B Touchpad"&lt;/span&gt;
&lt;span class="nv"&gt;TRACKPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"TPPS/2 Elan TrackPoint"&lt;/span&gt;
&lt;span class="nv"&gt;PRIMARY_PASTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Gtk/EnablePrimaryPaste"&lt;/span&gt;

xinput disable &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOUCHPAD&lt;/span&gt;&lt;span class="s2"&gt;"&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;xfconf-query &lt;span class="nt"&gt;-c&lt;/span&gt; xsettings &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIMARY_PASTE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; bool &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;false &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; xfconf-query &lt;span class="nt"&gt;-c&lt;/span&gt; xsettings &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIMARY_PASTE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;false

&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;xinput list &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Fxq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TRACKPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;xinput set-prop &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TRACKPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"libinput Scroll Method Enabled"&lt;/span&gt; 0 0 1 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;xinput set-prop &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TRACKPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"libinput Button Scrolling Button"&lt;/span&gt; 2 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;xinput set-button-map &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TRACKPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 1 0 3 4 5 6 7 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;Then I registered it with Xfce autostart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Desktop Entry]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="py"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ThinkPadder Mode In&lt;/span&gt;
&lt;span class="py"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Enter TrackPoint-first ThinkPad input mode&lt;/span&gt;
&lt;span class="py"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/rbcn2000/.local/bin/thinkpadder-mode-in.sh&lt;/span&gt;
&lt;span class="py"&gt;OnlyShowIn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;XFCE;&lt;/span&gt;
&lt;span class="py"&gt;X-GNOME-Autostart-enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;Terminal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the machine starts, it enters ThinkPadder Mode automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constraints Are Interfaces
&lt;/h3&gt;

&lt;p&gt;This is not for everyone.&lt;/p&gt;

&lt;p&gt;Many people are better off with the touchpad. Some people love X11 middle-click paste. Some people want the escape route visible at all times.&lt;/p&gt;

&lt;p&gt;But for me, cutting off the retreat and fixing the operation path was rational.&lt;/p&gt;

&lt;p&gt;I wanted fewer misfires. I wanted a stable input posture. I wanted to return to a TrackPoint-centered bodily feel. For that goal, reducing options was not merely making the machine less convenient.&lt;/p&gt;

&lt;p&gt;The constraint was the interface.&lt;/p&gt;

&lt;p&gt;UX is not only about adding what users can do. Sometimes the body stops hesitating because something becomes impossible. Disable the touchpad, and the hand returns to the red dot. Stop delivering Button2 to applications, and the middle button becomes trustworthy as a scrolling modifier.&lt;/p&gt;

&lt;p&gt;Customization is not only about making the tool adapt to you.&lt;/p&gt;

&lt;p&gt;There is also a kind of customization where you make your body adapt to the tool's philosophy.&lt;/p&gt;

&lt;p&gt;The TrackPoint is not a substitute for a mouse.&lt;/p&gt;

&lt;p&gt;It is an extension of the keyboard.&lt;/p&gt;

&lt;p&gt;If you see it that way, disabling the touchpad is not just nostalgia. It is a way to govern input paths and support embodied use.&lt;/p&gt;




&lt;p&gt;This essay looked at TrackPoint UX through the lens of constraints and embodied tool design.&lt;/p&gt;

&lt;p&gt;The more personal memory behind the same experience is here.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://fragmentofview.rbcn.cc/posts/en/red-dot-in-the-middle-touchpad/" rel="noopener noreferrer"&gt;https://fragmentofview.rbcn.cc/posts/en/red-dot-in-the-middle-touchpad/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Japanese version:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://emptytheory.rbcn.cc/ja/constraints-are-interfaces-trackpoint-ux/" rel="noopener noreferrer"&gt;https://emptytheory.rbcn.cc/ja/constraints-are-interfaces-trackpoint-ux/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>emptytheory</category>
      <category>interfacedesign</category>
      <category>ux</category>
      <category>embodiedcognition</category>
    </item>
    <item>
      <title>Running Obsidian Community Plugins in Flatpak — Battle Notes and a Move to AppImage</title>
      <dc:creator>rbcn</dc:creator>
      <pubDate>Sun, 14 Jun 2026 15:33:28 +0000</pubDate>
      <link>https://dev.to/rbcn/running-obsidian-community-plugins-in-flatpak-battle-notes-and-a-move-to-appimage-4c1f</link>
      <guid>https://dev.to/rbcn/running-obsidian-community-plugins-in-flatpak-battle-notes-and-a-move-to-appimage-4c1f</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;For a while now, I've been running Obsidian in Flatpak on Linux.&lt;/p&gt;

&lt;p&gt;The reason was simple: easier package management, a clean system thanks to sandboxing, and better update discipline than dropping a random binary into &lt;code&gt;/usr/local/bin&lt;/code&gt;. For a proprietary app like Obsidian that ships its own distribution, Flatpak just makes sense. That was all there was to it.&lt;/p&gt;

&lt;p&gt;Then I started using community plugins seriously — and everything changed.&lt;/p&gt;

&lt;p&gt;Git sync, AI coding assistance, local CLI integration: almost every plugin that does something interesting assumes it can spawn subprocesses in the same environment as your shell. Flatpak's sandbox quietly breaks that assumption.&lt;/p&gt;

&lt;p&gt;This is the battle log.&lt;/p&gt;




&lt;h3&gt;
  
  
  Flatpak's Sandbox Looks Transparent. It Isn't.
&lt;/h3&gt;

&lt;p&gt;Let me set the stage.&lt;/p&gt;

&lt;p&gt;Flatpak runs applications in an isolated sandbox. Your home directory is mounted. The network works by default. In day-to-day use you barely notice the sandbox at all.&lt;/p&gt;

&lt;p&gt;The problems start when an app tries to spawn a subprocess.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What Flatpak restricts:
  - Process namespace (isolated from host processes)
  - Filesystem (only explicitly granted paths are accessible)
  - Environment variables (host shell settings don't reach the sandbox)

What Flatpak passes through:
  - Network (allowed by default)
  - Home directory (mounted)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"The file is right there but I can't execute it." "The command exists but it's not in PATH." These are classic Flatpak symptoms, born from this gap. Error messages often don't surface either, which makes debugging hard.&lt;/p&gt;




&lt;h3&gt;
  
  
  Battle One: Obsidian Git and the SSH Agent
&lt;/h3&gt;

&lt;p&gt;The first wall was SSH authentication in Obsidian Git.&lt;/p&gt;

&lt;p&gt;Connecting to GitHub from the terminal worked fine. But the Obsidian Git plugin kept asking for a passphrase, or failing authentication entirely.&lt;/p&gt;

&lt;p&gt;The cause was the reach of &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you start &lt;code&gt;ssh-agent&lt;/code&gt; in a terminal, the &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; it exports only reaches that shell's child processes. Flatpak Obsidian, launched from the GNOME app launcher, sits outside that process tree entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal's ssh-agent
  └─ terminal process
      └─ zsh (SSH_AUTH_SOCK is set)
                              ← wall is here
Flatpak Obsidian (SSH_AUTH_SOCK never arrives)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried unifying everything through GNOME Keyring (gcr), but ran into a different problem: gcr-ssh-agent hung during the signing operation in a way I couldn't reliably fix.&lt;/p&gt;

&lt;p&gt;The solution that stuck was &lt;strong&gt;running OpenSSH ssh-agent as a systemd user service&lt;/strong&gt;, giving it a fixed socket path that exists before any shell or GUI app starts.&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;# ~/.config/systemd/user/ssh-agent.service&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OpenSSH key agent

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;span class="nv"&gt;Environment&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;%t/ssh-agent.socket
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/ssh-agent &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; %t/ssh-agent.socket

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a fixed socket at &lt;code&gt;/run/user/1000/ssh-agent.socket&lt;/code&gt;, I could expose it to Flatpak explicitly and reference it in a dedicated Obsidian Git wrapper script with &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; and &lt;code&gt;BatchMode=yes&lt;/code&gt; hardcoded.&lt;/p&gt;

&lt;p&gt;The full story is in the previous post:&lt;br&gt;
&lt;strong&gt;→ &lt;a href="https://emptytheory.rbcn.cc/en/migrating-to-zsh-broke-obsidian-git" rel="noopener noreferrer"&gt;Migrating to zsh Broke Obsidian Git — A Battle with SSH Agent and Flatpak&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Battle Two: The Codex CLI and stdio JSON-RPC
&lt;/h3&gt;

&lt;p&gt;With SSH sorted, things were stable for a while. Then AI plugin integration became my next problem.&lt;/p&gt;

&lt;p&gt;When you use AI coding assistance inside Obsidian, how it works depends entirely on the plugin — and the differences matter a lot in Flatpak.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin + Integration&lt;/th&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;CLI runtime&lt;/th&gt;
&lt;th&gt;Flatpak impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Smart Composer + Codex&lt;/td&gt;
&lt;td&gt;HTTP API (REST)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Network works → no problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claudian + Claude Code&lt;/td&gt;
&lt;td&gt;stdio JSON-RPC&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bun standalone ELF&lt;/strong&gt; (self-contained)&lt;/td&gt;
&lt;td&gt;Worked from day one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claudian + Codex CLI&lt;/td&gt;
&lt;td&gt;stdio JSON-RPC&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Node.js script&lt;/strong&gt; (&lt;code&gt;#!/usr/bin/env node&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Broke&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Smart Composer with a Codex subscription worked out of the box because it &lt;strong&gt;calls the OpenAI REST API over the network&lt;/strong&gt;. Flatpak doesn't block network access.&lt;/p&gt;

&lt;p&gt;Claudian's Codex integration didn't, because it &lt;strong&gt;spawns the local &lt;code&gt;codex&lt;/code&gt; CLI as a subprocess and communicates over stdio using JSON-RPC&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;Claudian plugin
  → spawn: codex app-server --listen stdio://
  → JSON-RPC over stdin/stdout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  I Wrote a Wrapper Script
&lt;/h4&gt;

&lt;p&gt;To call &lt;code&gt;/home/rbcn2000/.npm-global/bin/codex&lt;/code&gt; from inside Flatpak, I needed &lt;code&gt;flatpak-spawn --host&lt;/code&gt; to launch it as a host-side process. My first attempt:&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;#!/usr/bin/env zsh&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;flatpak-spawn &lt;span class="nt"&gt;--host&lt;/span&gt; zsh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'/home/rbcn2000/.npm-global/bin/codex "$@"'&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-i&lt;/code&gt; (interactive) flag was there to load &lt;code&gt;.zshrc&lt;/code&gt; and get nvm's Node.js into PATH. That was the mistake.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;zsh -i&lt;/code&gt; Poisons stdout
&lt;/h4&gt;

&lt;p&gt;I set this as the Codex CLI path in Claudian's settings. The integration still didn't work. No error message.&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;zsh -i&lt;/code&gt; starts as an interactive shell, which means it executes &lt;code&gt;.zshrc&lt;/code&gt; in full. My &lt;code&gt;.zshrc&lt;/code&gt; includes:&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;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="c"&gt;# may write to stdout&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="c"&gt;# writes initialization output to stdout&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;$NVM_DIR&lt;/span&gt;&lt;span class="s2"&gt;/nvm.sh"&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="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVM_DIR&lt;/span&gt;&lt;span class="s2"&gt;/nvm.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claudian's Codex integration &lt;strong&gt;starts reading stdout as a JSON-RPC stream the moment the process launches&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;Expected stdout:
  {"jsonrpc":"2.0","id":1,"result":{...}}

Actual stdout:
  (starship / zoxide initialization output)
  {"jsonrpc":"2.0","id":1,"result":{...}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON parser fails on the first non-JSON line. The connection drops. The error is reported as "could not connect" — with no hint about what was on stdout. The root cause is invisible.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Fix: Remove the Intermediate Shell
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nv"&gt;HOST_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/bin:/usr/local/bin:/home/rbcn2000/.npm-global/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;flatpak-spawn &lt;span class="nt"&gt;--host&lt;/span&gt; &lt;span class="nb"&gt;env &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;$HOST_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  node /home/rbcn2000/.npm-global/lib/node_modules/@openai/codex/bin/codex.js &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No intermediate shell&lt;/strong&gt; — nothing can pollute stdout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;node&lt;/code&gt; called directly&lt;/strong&gt; — skips symlink resolution and shebang lookup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATH built explicitly for the host&lt;/strong&gt; — Flatpak's sandbox PATH doesn't include &lt;code&gt;/usr/bin&lt;/code&gt; or npm-global, so I assemble the environment that &lt;code&gt;flatpak-spawn --host&lt;/code&gt; will pass to the host process myself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That worked.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Claude Code Worked From the Start
&lt;/h4&gt;

&lt;p&gt;It's worth asking why Claudian's Claude Code integration never needed any of this. The answer is a fundamental difference in how the two tools are built.&lt;/p&gt;

&lt;p&gt;Inspecting the Claude Code binary:&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="nv"&gt;$ &lt;/span&gt;file ~/.local/share/claude/versions/2.1.177
ELF 64-bit LSB executable, x86-64, dynamically linked

&lt;span class="nv"&gt;$ &lt;/span&gt;ldd ~/.local/share/claude/versions/2.1.177
  librt.so.1, libc.so.6, libpthread.so.0, libdl.so.2, libm.so.6

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; ~/.local/share/claude/versions/2.1.177
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt;  239M  claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies: only &lt;code&gt;libc&lt;/code&gt; family. No &lt;code&gt;libnode.so&lt;/code&gt;. No &lt;code&gt;libv8.so&lt;/code&gt;. Size: &lt;strong&gt;239 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the signature of a &lt;strong&gt;Bun &lt;code&gt;bun build --compile&lt;/code&gt; standalone ELF&lt;/strong&gt;. Bun statically bundles its JavaScript engine (JavaScriptCore) directly into the binary at compile time. The result needs no external Node.js runtime — it's fully self-contained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code (Bun standalone):
  ELF binary
    └─ JavaScriptCore (statically bundled)
    └─ application code (statically bundled)
  → doesn't care what's in PATH
  → runs from anywhere, including Flatpak's sandbox

Codex CLI (Node.js script):
  codex.js  ← #!/usr/bin/env node
    → looks up `node` in PATH at runtime
    → nvm's node isn't in Flatpak's PATH
    → fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If Codex moves to a native Rust binary&lt;/strong&gt;, the story changes again. A Rust binary is also self-contained, with no external runtime dependency. The wrapper problem would simply disappear.&lt;/p&gt;

&lt;p&gt;The discussion of "Claude Code is Bun, Codex is moving to Rust" has been floating around lately — but for Flatpak users, it maps directly onto a concrete lived experience: &lt;em&gt;why did one work and the other didn't?&lt;/em&gt; An abstract build-system choice showed up as the difference between "works on first try" and "two hours of silent debugging."&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Community Plugins Don't Handle Flatpak
&lt;/h3&gt;

&lt;p&gt;After these two battles, the structural reasons became clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Flatpak Obsidian users are a thin slice of a thin slice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Linux desktop users are a minority among Obsidian's userbase. Flatpak Obsidian users are a minority among Linux desktop users. If you're a plugin developer on macOS, Windows, or even a &lt;code&gt;.deb&lt;/code&gt;/AppImage Linux setup, you'll never encounter Flatpak's behavior. There's no incentive to test for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② Flatpak failures are invisible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;command not found&lt;/code&gt; or &lt;code&gt;permission denied&lt;/code&gt; are easy to debug. Flatpak problems tend to appear as &lt;strong&gt;timeouts&lt;/strong&gt; and &lt;strong&gt;silent connection failures&lt;/strong&gt; — exactly the symptoms both of my issues produced. It's hard to file a useful bug report for "it just doesn't connect," and hard for a developer to reproduce without a Flatpak environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ Plugins assume "same environment as the shell"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any plugin that spawns a subprocess implicitly assumes that &lt;code&gt;PATH&lt;/code&gt;, environment variables, and sockets like &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; have the same values as in a terminal session. Flatpak silently breaks that assumption.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Decision: Move to AppImage
&lt;/h3&gt;

&lt;p&gt;Every workaround I added made the overall setup more fragile.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For SSH agent: a systemd service, a wrapper script, a &lt;code&gt;.desktop&lt;/code&gt; file override&lt;/li&gt;
&lt;li&gt;For Codex CLI: a &lt;code&gt;flatpak-spawn&lt;/code&gt; wrapper, manually assembled PATH&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each fix is correct in isolation. Together, they form a web of dependencies where changing one thing can break another. Upgrade nvm → PATH shifts. Adjust systemd → socket path changes.&lt;/p&gt;

&lt;p&gt;AppImage is a different approach. It doesn't sandbox anything. It runs as a regular process in the host environment, inheriting your PATH, your &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;, your nvm — everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AppImage Obsidian:
  PATH              → inherited from host
  SSH_AUTH_SOCK     → inherited from host
  subprocess spawn  → runs in host environment
  flatpak-spawn     → not needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The maintenance calculus has flipped. AppImage is now the simpler choice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;p&gt;Pushing Flatpak Obsidian to its limits produced two real lessons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH agent&lt;/strong&gt;: Environment variables don't teleport — they propagate through process inheritance. A socket that works in your terminal doesn't exist in a GUI app spawned from a launcher. Anchoring the agent to a fixed path via systemd removes the dependency on how the app was launched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex CLI / stdio JSON-RPC&lt;/strong&gt;: When a plugin communicates over stdio as a protocol, the spawned process must produce nothing but protocol output. An interactive shell between the plugin and the binary is a silent killer — init scripts write to stdout, the JSON parser fails, the connection drops with no error trace.&lt;/p&gt;

&lt;p&gt;Neither failure was Flatpak's fault. Flatpak sandboxed exactly what it was supposed to sandbox. The failure was in not accounting for those boundaries earlier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never put an interactive shell between a caller and a subprocess that uses stdio as a protocol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the single most concrete rule I can extract from this.&lt;/p&gt;

&lt;p&gt;And the broader one: &lt;strong&gt;workarounds compound. At some point, the maintenance cost of staying exceeds the switching cost of leaving.&lt;/strong&gt; Moving to AppImage is an acknowledgment of reaching that point.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>flatpak</category>
      <category>obsidian</category>
      <category>node</category>
    </item>
    <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>
