cd /news/ai-agents/shipping-a-self-contained-macos-app-… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-131471] src=dev.to β†— pub= topic=ai-agents verified=true sentiment=↑ positive

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.

by read5 min views3 publishedSep 16, 2026

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 @_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 (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

$ 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
}
install_name_tool -change /opt/homebrew/opt/llhttp/lib/libllhttp.9.4.dylib \
                              @_path/../lib/libllhttp.9.4.dylib  bin/node

install_name_tool -change /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib \
                              @_path/libcrypto.3.dylib            lib/libnode.147.dylib

Two libraries slip past an absolute-path grep because Homebrew already references them relatively:

libicuuc β†’ @_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:

$ 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 @_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.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @deepseek harness 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/shipping-a-self-cont…] indexed:0 read:5min 2026-09-16 Β· β€”