# Shipping a Self-Contained macOS App: How 24 Homebrew dylibs Broke My DeepSeek Harness Installer

> Source: <https://dev.to/hi_wonderful/shipping-a-self-contained-macos-app-how-24-homebrew-dylibs-broke-my-deepseek-harness-installer-4i8b>
> Published: 2026-09-16 13:33:16+00:00

**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.*
