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.