Shipping a Self-Contained macOS App: How 24 Homebrew dylibs Broke My DeepSeek Harness Installer A developer built a self-contained macOS installer for the open-source DeepSeek Harness AI agent workbench, only to discover that the bundled Node binary linked 24 Homebrew dynamic libraries by absolute path, causing the app to crash silently on machines without Homebrew. The fix involved recursively scanning the dependency graph with otool, copying every dylib into the app bundle, rewriting paths to @loader_path, and re-signing, which shrank the installer from 792 MB to 620 MB while adding three plugins. The build scripts and repo are open source. TL;DR — 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. otool -L revealed 24 dynamic libraries linked by absolute Homebrew paths. Fixing it meant copying every dylib into the app bundle, rewriting paths to @loader path , and re-signing. The installer got smaller 792 MB → 620 MB while gaining three plugins. Repo and build scripts are open source. npm i -g " DeepSeek Harness https://github.com/deepseek-ai/deepseek-harness dsh 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: npm i -g @deepseek-ai/dsh requires Node dsh web then start the server and open a browser 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 macOS app you install by double-clicking : mount the DMG → double-click "Install" → the GUI opens fullscreen, no questions asked. Version 0.1.0 came together in an evening. Then I tested it on a machine that had never seen Homebrew. The Electron main process is deliberately boring: ~/.dsh doesn't exist, copy the bundled seed profile into place instant out-of-the-box experience for new users . 127.0.0.1:3080 ; if nothing is listening, boot dsh web using the bundled Node binary. The bundle layout: 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 I assumed that bundling node meant Homebrew was irrelevant. On the clean Mac, the app vanished in about a second, and the log file was empty. Not missing — empty. Electron never even got far enough to write anything. otool -L Finds the Culprit bash $ otool -L runtime/bin/node @rpath/libnode.147.dylib /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib ← absolute path /opt/homebrew/opt/libuv/lib/libuv.1.dylib ← absolute path /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib ← absolute path ... 24 in total Homebrew's node build links 24 dynamic libraries by absolute path under /opt/homebrew/opt/ /lib — OpenSSL, ICU, llhttp, libuv, simdjson, brotli, c-ares, zstd, SQLite, ngtcp2/nghttp3, and friends. No Homebrew on the target machine means dyld can't resolve them, and the process aborts before Electron can log a thing. So 0.1.0's "no Homebrew required" claim was simply false. 😅 Lesson: copying a binary into your app bundle does not make it self-contained. You have to walk the dependency graph. One otool -L pass isn't enough: dylibs depend on other dylibs node → libnode → icu → icudata . scan { otool -L "$1" | tail -n +2 | awk '/\/opt\/homebrew\//{print $1}' | while read -r d; do grep -qxF "$d" list.txt || { echo "$d" list.txt; scan "$d"; } done } bin/node lives in bin/ → relative path to lib is ../lib install name tool -change /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib \ @loader path/../lib/libllhttp.9.4.dylib bin/node dylibs inside lib/ → same directory install name tool -change /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib \ @loader path/libcrypto.3.dylib lib/libnode.147.dylib Two libraries slip past an absolute-path grep because Homebrew already references them relatively: libicuuc → @loader path/libicudata.78.dylib libbrotlidec / libbrotlienc → @rpath/libbrotlicommon.1.dylib Copy those in by hand and rewrite the @rpath reference. Then, because you've modified Mach-O binaries inside an app bundle, on Apple Silicon you must re-sign ad hoc: codesign --force --deep --sign - Skip that step and the OS sends SIGKILL — which looks exactly like the original bug: instant quit, empty logs. Verification: bash $ otool -L bin/node | grep homebrew empty — all green ✅ $ runtime/bin/node --version v26.5.0 ✅ All of this is wrapped in a reusable script: scripts/bundle-homebrew-deps.sh . While I was in there: dsh-vision-router to 2.1.4. agent-presets-seed/ directory is imported into ~/.dsh/.agent-presets on first launch. It only copies what's missing and never overwrites a user's own presets. 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 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. ~/Applications , initializes No Node, no Homebrew, no admin password, fully offline. libicudata and brotli's libbrotlicommon are referenced via @loader path / @rpath and won't show up in an absolute-path grep. install name tool invalidates the signature; on Apple Silicon the OS kills unsigned binaries outright — symptom: instant quit, no logs. app.getPath 'home' doesn't always respect $HOME . du -sh every Resources subdirectory — duplicate directories hide easily. The interesting part of this project wasn't the Electron shell — it was discovering that "self-contained" is a claim you have to prove , not assume. A 24-line dependency scan and a codesign call were the difference between an app that works on the author's laptop and one that works on a stranger's. 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. 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." Not affiliated with DeepSeek. DeepSeek Harness and its plugins belong to their respective authors.