clowner: Turning a macOS App Into Two Independent Copies
One Bash script that copies a .app into a second copy with its own bundle identifier — two Braves, two Slacks, two of anything, running side by side with separate data.
The problem: I wanted two genuinely separate browsers — one personal, one for work — sharing no session, and without switching profiles every time.
macOS identifies an app by the CFBundleIdentifier in its Info.plist. Two copies with the same identifier are treated as the same app; change the identifier and the copy becomes a different app as far as the system is concerned. The catch is that editing Info.plist breaks the original code signature — so the clone has to be re-signed.
clowner does both. One Bash script, calling tools that already exist on every Mac.
curl -fsSL https://raw.githubusercontent.com/arisros/clowner/main/install.sh | sh
clowner # no arguments = wizard
What it does
- Copies the bundle with
cp -a. - Rewrites
CFBundleIdentifierin everyInfo.plistinside the bundle whose identifier starts with the original’s — the app itself plus its helpers (…helper,…helper.renderer). Unrelated nested bundles are left alone. - Strips the auto-update keys (
KSProductID,SUFeedURL, …). - If the clone needs to run as its own instance, replaces
Contents/MacOS/<exe>with a two-line wrapper that execs the real binary with isolation flags. - Clears quarantine attributes, then re-signs:
codesign --force --deep --sign -.
Three things I didn’t expect
The auto-update keys have to go. Otherwise the clone and the original both point at the same Sparkle or Keystone feed and start updating over each other. There’s a --keep-updater for anyone who actually wants that, but the default is to strip them.
An app’s name isn’t only in Info.plist. Apps that localise their display name — the iWork apps, and plenty of others — store it in Contents/Resources/*.lproj/InfoPlist.strings, and that value wins over Info.plist. Until that surfaced, the menu bar and Finder kept showing the original name even with Info.plist rewritten.
The icon has its own shortcut too. Modern apps load their icon from a compiled Assets.car via CFBundleIconName, which overrides a plain .icns. So when --icon is used, the CFBundleIconName key is dropped as well, so the new .icns is what actually gets used. And the icon swap has to happen before re-signing, or the signature is invalid again immediately.
Not just browsers
The core — copy, re-identify, re-sign — works on any .app. Browsers are just the case that needs one extra thing: a flag so the second copy opens its own window instead of handing the command to the instance already running. Chromium apps are detected automatically and get --user-data-dir; Firefox/Gecko apps get -no-remote --profile.
Ordinary apps need no wrapper at all and clone bare with --no-profile. For anything with its own single-instance lock, --args takes whatever flags it needs:
clowner clone --src "/Applications/Foo.app" --name foo2 --args "--its-own-flag=$HOME/foo2"
Why Bash
Nothing is compiled and there’s no runtime to install. The script runs on macOS’s stock /bin/bash — version 3.2, which is well over a decade old — so there’s no “first install a newer Bash” step in front of a tool whose whole job is copying a folder. fzf is optional: with it the app picker is fuzzy-searchable, without it you get a numbered menu.
After cloning, macOS sometimes keeps the old name and icon cached. touch "/Applications/<clone>.app" followed by killall Dock Finder usually settles it.