{"slug": "shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek", "title": "Shipping a Self-Contained macOS App: How 24 Homebrew dylibs Broke My DeepSeek Harness Installer", "summary": "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.", "body_md": "**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.\n\n`npm i -g`\"\n[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:\n\n```\nnpm i -g @deepseek-ai/dsh   # requires Node\ndsh web                     # then start the server and open a browser\n```\n\nThat'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.\n\nVersion 0.1.0 came together in an evening. Then I tested it on a machine that had never seen Homebrew.\n\nThe Electron main process is deliberately boring:\n\n`~/.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.\nThe bundle layout:\n\n```\nDeepSeek Harness.app/\n└── Contents/Resources/\n    ├── app/main.js          # Electron main process: self-healing logic\n    ├── runtime/bin/node     # bundled Node 26.5.0 (arm64)\n    ├── runtime/dsh/         # dsh CLI + 194 npm dependencies\n    └── profile-seed-web/    # clean web profile seed (plugins)\n```\n\nI 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.\n\n`otool -L` Finds the Culprit\n\n``` bash\n$ otool -L runtime/bin/node\n    @rpath/libnode.147.dylib\n    /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib      # ← absolute path!\n    /opt/homebrew/opt/libuv/lib/libuv.1.dylib             # ← absolute path!\n    /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib     # ← absolute path!\n    ... (24 in total)\n```\n\nHomebrew'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.\n\nSo 0.1.0's \"no Homebrew required\" claim was simply false. 😅\n\n**Lesson:** copying a binary into your app bundle does **not** make it self-contained. You have to walk the dependency graph.\n\nOne `otool -L` pass isn't enough: dylibs depend on other dylibs (`node → libnode → icu → icudata`).\n\n```\nscan() {\n  otool -L \"$1\" | tail -n +2 | awk '/\\/opt\\/homebrew\\//{print $1}' | while read -r d; do\n    grep -qxF \"$d\" list.txt || { echo \"$d\" >> list.txt; scan \"$d\"; }\n  done\n}\n# bin/node lives in bin/ → relative path to lib is ../lib\ninstall_name_tool -change /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib \\\n                              @loader_path/../lib/libllhttp.9.4.dylib  bin/node\n\n# dylibs inside lib/ → same directory\ninstall_name_tool -change /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib \\\n                              @loader_path/libcrypto.3.dylib            lib/libnode.147.dylib\n```\n\nTwo libraries slip past an absolute-path grep because Homebrew already references them relatively:\n\n`libicuuc` → `@loader_path/libicudata.78.dylib`\n`libbrotlidec`/` libbrotlienc` → `@rpath/libbrotlicommon.1.dylib`\nCopy 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:\n\n```\ncodesign --force --deep --sign -\n```\n\nSkip that step and the OS sends `SIGKILL` — which looks exactly like the original bug: instant quit, empty logs.\n\nVerification:\n\n``` bash\n$ otool -L bin/node | grep homebrew   # empty — all green ✅\n$ runtime/bin/node --version          # v26.5.0 ✅\n```\n\nAll of this is wrapped in a reusable script: `scripts/bundle-homebrew-deps.sh`.\n\nWhile I was in there:\n\n`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.\n\n```\nDeepSeek Harness.app/Contents/Resources/\n├── app/main.js                    # Electron main process\n├── runtime/bin/node               # Node 26.5.0 (arm64, self-contained)\n├── runtime/lib/*.dylib            # libnode + 24 bundled dylibs\n├── runtime/dsh/                   # dsh CLI v0.1.0-rc.6 + 194 packages\n├── profile-seed-web/              # clean web profile seed\n└── agent-presets-seed/            # user-level agent presets\n```\n\nPlugins 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.\n\n`~/Applications`, initializes No Node, no Homebrew, no admin password, fully offline.\n\n`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.\nThe 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.\n\nIf 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.\n\nThe 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.\"\n\n*Not affiliated with DeepSeek. DeepSeek Harness and its plugins belong to their respective authors.*", "url": "https://wpnews.pro/news/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek", "canonical_source": "https://dev.to/hi_wonderful/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek-harness-installer-4i8b", "published_at": "2026-09-16 13:33:16+00:00", "updated_at": "2026-09-16 13:43:10.422057+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["DeepSeek Harness", "DeepSeek", "Homebrew", "Electron", "Node.js", "macOS", "npm", "otool"], "alternates": {"html": "https://wpnews.pro/news/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek", "markdown": "https://wpnews.pro/news/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek.md", "text": "https://wpnews.pro/news/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek.txt", "jsonld": "https://wpnews.pro/news/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek.jsonld"}}