This is a practical, reality-tested method for running multiple independent instances of the same macOS app with separate local data (sessions, cache, settings, extensions, etc.).
It deliberately avoids false guarantees:
- This does not create full OS-level isolation
- This does not isolate Keychain, iCloud, or permissions
- This works only if the app supports alternate data directories or profiles
If the app ignores custom data paths or is heavily sandboxed, this approach will fail. In that case, use a separate macOS user account.
- Chromium-based apps (Chrome, Edge, Brave)
- Many Electron apps (case-by-case)
- Developer tools (VS Code, etc.)
--user-data-dir- built-in profiles
- portable mode
- Mac App Store apps (sandboxed)
- apps using system containers (
~/Library/Containers) - apps enforcing single-instance locks
- apps with background daemons or shared services
You are not cloning the app.
You are launching the same binary with a different data root.
That is the only mechanism that matters.
Create one isolated root per clone:
mkdir -p "$HOME/AppClones/Work-App"/{home,profile,logs}
mkdir -p "$HOME/AppClones/Personal-App"/{home,profile,logs}Structure:
AppClones/
Work-App/
home/ # optional alt HOME
profile/ # actual app data
logs/
Do not rely on open.
Find the actual binary:
ls "/Applications/AppName.app/Contents/MacOS/"Example:
/Applications/Google Chrome.app/Contents/MacOS/Google ChromeCreate:
nano ~/AppClones/Work-App/launch.commandPaste:
#!/bin/zsh
set -euo pipefail
ORIG_HOME="$HOME"
CLONE_ROOT="$ORIG_HOME/AppClones/Work-App"
ALT_HOME="$CLONE_ROOT/home"
PROFILE_DIR="$CLONE_ROOT/profile"
LOG_DIR="$CLONE_ROOT/logs"
mkdir -p "$ALT_HOME" "$PROFILE_DIR" "$LOG_DIR"
APP="/Applications/AppName.app/Contents/MacOS/AppName"
exec env HOME="$ALT_HOME" \
"$APP" \
--user-data-dir="$PROFILE_DIR" \
"$@" \
>>"$LOG_DIR/stdout.log" 2>>"$LOG_DIR/stderr.log"Make executable:
chmod +x ~/AppClones/Work-App/launch.commandUse:
--user-data-dir="$PROFILE_DIR"This is the official mechanism for isolating browser state.
Do NOT use the script method.
Use built-in profiles:
about:profilesor:
firefox -PUse Portable Mode, not cloning:
Visual Studio Code.app/
code-portable-data/
This keeps all data local to that instance.
Try:
--user-data-dir="$PROFILE_DIR"Outcomes:
- Works → full isolation
- Ignores flag → not cloneable via this method
- Forces single instance → not cloneable
Use Script Editor (built-in).
- Open Script Editor
- Paste:
set h to POSIX path of (path to home folder)
do shell script quoted form of (h & "AppClones/Work-App/launch.command") & " >/dev/null 2>&1 &"-
Export:
- File → Export
- Format: Application
- Name:
Work App.app
mkdir -p ~/Applications
mv "Work App.app" ~/Applications/Then:
- Drag to Dock
- Launch via Spotlight
- Treat as independent app
- Original app
- Clone
| Check | Expected |
|---|---|
| Login sessions | Different accounts |
| Cookies/storage | Separate |
| Extensions/plugins | Independent |
| Files written | Inside ~/AppClones/.../profile |
| Logs | Written to logs/ |
Inspect:
ls ~/AppClones/Work-App/profileCheck logs:
tail -n 100 ~/AppClones/Work-App/logs/stderr.logCommon failures:
| Symptom | Cause |
|---|---|
| Same session as original | app ignored data dir |
| Only one instance opens | single-instance lock |
| Data still shared | app uses container / external service |
| App crashes | invalid flag |
Backup:
rsync -a "$HOME/AppClones/" "$HOME/Backups/AppClones/"Delete clone:
rm -rf ~/AppClones/Work-AppUpdate app:
- no change required
- script still points to original binary
This method cannot override:
- macOS App Sandbox
- app-group containers
- background services
- system-level credential storage
- single-instance enforcement
If any of those apply, the clone is not truly isolated.
If the app supports profiles:
- Chrome → Profiles
- Firefox → Profiles
- VS Code → Profiles / Portable Mode
These are:
- more stable
- officially supported
- zero maintenance
If you need:
- separate permissions
- separate app containers
- separate OS-level state
Use:
System Settings → Users → Add User
Then enable Fast User Switching.
This method works when:
- the app allows a custom data directory
It fails when:
- the app does not
There is no workaround for that boundary without moving up to OS-level isolation.