<?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: hi_wonderful</title>
    <description>The latest articles on DEV Community by hi_wonderful (@hi_wonderful).</description>
    <link>https://dev.to/hi_wonderful</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%2F4122132%2Fc8a14591-4760-4d48-a71e-f3761227c5aa.png</url>
      <title>DEV Community: hi_wonderful</title>
      <link>https://dev.to/hi_wonderful</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hi_wonderful"/>
    <language>en</language>
    <item>
      <title>Shipping a Self-Contained macOS App: How 24 Absolute Library Paths Broke My DeepSeek Harness Installer</title>
      <dc:creator>hi_wonderful</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:33:16 +0000</pubDate>
      <link>https://dev.to/hi_wonderful/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek-harness-installer-4i8b</link>
      <guid>https://dev.to/hi_wonderful/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek-harness-installer-4i8b</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I wrapped an npm-distributed AI agent workbench into a double-click macOS installer. It worked on my machine, then quit instantly with zero logs on a clean Mac. &lt;code&gt;otool -L&lt;/code&gt; revealed 24 dynamic libraries linked by absolute paths baked in at build time. Fixing it meant copying every dylib into the app bundle, rewriting paths to &lt;code&gt;@loader_path&lt;/code&gt;, and re-signing. The installer got &lt;em&gt;smaller&lt;/em&gt; (792 MB → 620 MB) while gaining three plugins. Repo and build scripts are open source.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With "Just &lt;code&gt;npm i -g&lt;/code&gt;"
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/deepseek-ai/deepseek-harness" rel="noopener noreferrer"&gt;DeepSeek Harness&lt;/a&gt; (&lt;code&gt;dsh&lt;/code&gt;) is an open-source full-stack AI agent workbench: sessions, a plugin marketplace, themes, agent presets, image/video generation, cost tracking. It's genuinely powerful — but the official distribution is an npm package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @deepseek-ai/dsh   &lt;span class="c"&gt;# requires Node&lt;/span&gt;
dsh web                     &lt;span class="c"&gt;# then start the server and open a browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a non-starter for anyone who has never opened a terminal. Some of the people I wanted to share it with fall into exactly that category, so I wrapped it into a &lt;strong&gt;macOS app you install by double-clicking&lt;/strong&gt;: mount the DMG → double-click "Install" → the GUI opens fullscreen, no questions asked.&lt;/p&gt;

&lt;p&gt;Version 0.1.0 came together in an evening. Then I tested it on a machine that had never seen Homebrew.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Worked on My Machine (the Worst Kind of Bug)
&lt;/h2&gt;

&lt;p&gt;The Electron main process is deliberately boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On first launch, if &lt;code&gt;~/.dsh&lt;/code&gt; doesn't exist, copy the bundled seed profile into place (instant out-of-the-box experience for new users).&lt;/li&gt;
&lt;li&gt;Probe &lt;code&gt;127.0.0.1:3080&lt;/code&gt;; if nothing is listening, boot &lt;code&gt;dsh web&lt;/code&gt; using the bundled Node binary.&lt;/li&gt;
&lt;li&gt;Open a fullscreen window on that URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bundle layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DeepSeek Harness.app/
└── Contents/Resources/
    ├── app/main.js          # Electron main process: self-healing logic
    ├── runtime/bin/node     # bundled Node 26.5.0 (arm64)
    ├── runtime/dsh/         # dsh CLI + 194 npm dependencies
    └── profile-seed-web/    # clean web profile seed (plugins)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I assumed that bundling &lt;code&gt;node&lt;/code&gt; meant Homebrew was irrelevant. On the clean Mac, the app vanished in about a second, and the log file was empty. Not &lt;em&gt;missing&lt;/em&gt; — empty. Electron never even got far enough to write anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;otool -L&lt;/code&gt; Finds the Culprit
&lt;/h2&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;otool &lt;span class="nt"&gt;-L&lt;/span&gt; runtime/bin/node
    @rpath/libnode.147.dylib
    /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib      &lt;span class="c"&gt;# ← absolute path!&lt;/span&gt;
    /opt/homebrew/opt/libuv/lib/libuv.1.dylib             &lt;span class="c"&gt;# ← absolute path!&lt;/span&gt;
    /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib     &lt;span class="c"&gt;# ← absolute path!&lt;/span&gt;
    ... &lt;span class="o"&gt;(&lt;/span&gt;24 &lt;span class="k"&gt;in &lt;/span&gt;total&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Homebrew's &lt;code&gt;node&lt;/code&gt; build links &lt;strong&gt;24 dynamic libraries by absolute path&lt;/strong&gt; under &lt;code&gt;/opt/homebrew/opt/*/lib&lt;/code&gt; — OpenSSL, ICU, llhttp, libuv, simdjson, brotli, c-ares, zstd, SQLite, ngtcp2/nghttp3, and friends. No Homebrew on the target machine means &lt;code&gt;dyld&lt;/code&gt; can't resolve them, and the process aborts before Electron can log a thing.&lt;/p&gt;

&lt;p&gt;So 0.1.0's "no Homebrew required" claim was simply false. 😅&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; copying a binary into your app bundle does &lt;strong&gt;not&lt;/strong&gt; make it self-contained. You have to walk the dependency graph.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Fix: Bundle Every dylib and Rewrite the Paths
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Collect dependencies recursively
&lt;/h3&gt;

&lt;p&gt;One &lt;code&gt;otool -L&lt;/code&gt; pass isn't enough: dylibs depend on other dylibs (&lt;code&gt;node → libnode → icu → icudata&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;scan&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  otool &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +2 | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/\/opt\/homebrew\//{print $1}'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; d&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qxF&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; list.txt &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; list.txt&lt;span class="p"&gt;;&lt;/span&gt; scan &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Copy them in and rewrite absolute paths to relative ones
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# bin/node lives in bin/ → relative path to lib is ../lib&lt;/span&gt;
install_name_tool &lt;span class="nt"&gt;-change&lt;/span&gt; /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib &lt;span class="se"&gt;\&lt;/span&gt;
                              @loader_path/../lib/libllhttp.9.4.dylib  bin/node

&lt;span class="c"&gt;# dylibs inside lib/ → same directory&lt;/span&gt;
install_name_tool &lt;span class="nt"&gt;-change&lt;/span&gt; /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib &lt;span class="se"&gt;\&lt;/span&gt;
                              @loader_path/libcrypto.3.dylib            lib/libnode.147.dylib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Plug the gaps, then re-sign
&lt;/h3&gt;

&lt;p&gt;Two libraries slip past an absolute-path grep because Homebrew already references them relatively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ICU: &lt;code&gt;libicuuc&lt;/code&gt; → &lt;code&gt;@loader_path/libicudata.78.dylib&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;brotli: &lt;code&gt;libbrotlidec&lt;/code&gt;/&lt;code&gt;libbrotlienc&lt;/code&gt; → &lt;code&gt;@rpath/libbrotlicommon.1.dylib&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy those in by hand and rewrite the &lt;code&gt;@rpath&lt;/code&gt; reference. Then, because you've modified Mach-O binaries inside an app bundle, on Apple Silicon you &lt;strong&gt;must&lt;/strong&gt; re-sign ad hoc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip that step and the OS sends &lt;code&gt;SIGKILL&lt;/code&gt; — which looks exactly like the original bug: instant quit, empty logs.&lt;/p&gt;

&lt;p&gt;Verification:&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;otool &lt;span class="nt"&gt;-L&lt;/span&gt; bin/node | &lt;span class="nb"&gt;grep &lt;/span&gt;homebrew   &lt;span class="c"&gt;# empty — all green ✅&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;runtime/bin/node &lt;span class="nt"&gt;--version&lt;/span&gt;          &lt;span class="c"&gt;# v26.5.0 ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this is wrapped in a reusable script: &lt;code&gt;scripts/bundle-homebrew-deps.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Plugins In, 172 MB Out
&lt;/h2&gt;

&lt;p&gt;While I was in there:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plugin set upgraded&lt;/strong&gt; — the 0.1.0 seed shipped 7 plugins; I added a video editor, AI image generation, and cost tracking, and bumped &lt;code&gt;dsh-vision-router&lt;/code&gt; to 2.1.4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundled agent presets&lt;/strong&gt; — a new &lt;code&gt;agent-presets-seed/&lt;/code&gt; directory is imported into &lt;code&gt;~/.dsh/.agent-presets&lt;/code&gt; on first launch. It only copies what's missing and never overwrites a user's own presets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slimmed down&lt;/strong&gt; — 0.1.0 accidentally embedded a duplicated 489 MB profile directory and an 875 MB empty partition inside the DMG. After cleanup: &lt;strong&gt;620 MB, down from 792 MB&lt;/strong&gt;, with &lt;em&gt;more&lt;/em&gt; functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy hardening&lt;/strong&gt; — the seed contains code, plugins, and a config skeleton. No API keys, no sessions, no history.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Ships Inside
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DeepSeek Harness.app/Contents/Resources/
├── app/main.js                    # Electron main process
├── runtime/bin/node               # Node 26.5.0 (arm64, self-contained)
├── runtime/lib/*.dylib            # libnode + 24 bundled dylibs
├── runtime/dsh/                   # dsh CLI v0.1.0-rc.6 + 194 packages
├── profile-seed-web/              # clean web profile seed
└── agent-presets-seed/            # user-level agent presets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins included out of the box: a Cyberpunk 2077 theme, a web UI kit, computer control, vision routing v2.1.4, a video studio, AI image generation, cost tracking, and the plugin marketplace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing It
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mount the DMG (Finder opens the window for you).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right-click → Open&lt;/strong&gt; the installer → click "Open" in the dialog. It isn't Apple-notarized, so a plain double-click is blocked by Gatekeeper — this is a one-time step.&lt;/li&gt;
&lt;li&gt;The installer copies the app to &lt;code&gt;~/Applications&lt;/code&gt;, initializes &lt;code&gt;~/.dsh&lt;/code&gt;, and launches the GUI fullscreen.&lt;/li&gt;
&lt;li&gt;Settings → Models → paste your own API key (DeepSeek or any compatible provider).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No Node runtime to install, no package manager, no admin password — fully offline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/cj5pvj4vjt-ux/DeepSeekHarness-Mac-Installer" rel="noopener noreferrer"&gt;https://github.com/cj5pvj4vjt-ux/DeepSeekHarness-Mac-Installer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/cj5pvj4vjt-ux/DeepSeekHarness-Mac-Installer/releases/latest" rel="noopener noreferrer"&gt;https://github.com/cj5pvj4vjt-ux/DeepSeekHarness-Mac-Installer/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Six Things I'd Tell My Past Self
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify self-containment recursively.&lt;/strong&gt; &lt;code&gt;otool -L&lt;/code&gt; shows one level. Dylibs depend on dylibs — script the sweep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homebrew has internal relative references.&lt;/strong&gt; ICU's &lt;code&gt;libicudata&lt;/code&gt; and brotli's &lt;code&gt;libbrotlicommon&lt;/code&gt; are referenced via &lt;code&gt;@loader_path&lt;/code&gt;/&lt;code&gt;@rpath&lt;/code&gt; and won't show up in an absolute-path grep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-sign after touching any binary.&lt;/strong&gt; &lt;code&gt;install_name_tool&lt;/code&gt; invalidates the signature; on Apple Silicon the OS kills unsigned binaries outright — symptom: instant quit, no logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron's single-instance lock will fool you.&lt;/strong&gt; If an app with the same bundle ID is already running, the new instance exits silently with code 0. Give test copies a different bundle ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app.getPath('home')&lt;/code&gt; doesn't always respect &lt;code&gt;$HOME&lt;/code&gt;.&lt;/strong&gt; On macOS it can return the real user home, so your "fresh user" test may be writing logs somewhere else entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit bundle size before you ship.&lt;/strong&gt; &lt;code&gt;du -sh&lt;/code&gt; every Resources subdirectory — duplicate directories hide easily.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The interesting part of this project wasn't the Electron shell — it was discovering that "self-contained" is a claim you have to &lt;em&gt;prove&lt;/em&gt;, not assume. A 24-line dependency scan and a &lt;code&gt;codesign&lt;/code&gt; call were the difference between an app that works on the author's laptop and one that works on a stranger's.&lt;/p&gt;

&lt;p&gt;If you're packaging anything with native dependencies for macOS, run the recursive scan before you ship. It takes two minutes and saves a very confusing bug report.&lt;/p&gt;

&lt;p&gt;The build scripts, the Electron main process, and the plugin-seeding logic are all in the repo — issues and PRs welcome, especially if you've solved the notarization problem more elegantly than "right-click → Open."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not affiliated with DeepSeek. DeepSeek Harness and its plugins belong to their respective authors.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>electron</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
