Skip to content
Aaron Alexander
Go back

Running Two Claude Desktop Apps on One Mac (One for Work, One for Personal)

I use Claude for two lives: my personal account and my work account. The desktop app only signs into one at a time, and constantly logging out and back in got old fast. What I actually wanted was a second, fully independent copy — call it Claude Work.app — that launches on its own, keeps its own login, runs side by side with my personal Claude, and even has a different icon so I don’t mix them up in the Dock.

It turns out this is very doable, and once it’s set up it maintains itself. Here’s the whole thing, including the three dead ends I hit, so you can recreate it on your own Mac.

Tested on macOS 26 (Apple Silicon). The signing details matter most on Apple Silicon; the general approach works on Intel too. Wherever you see YOURNAME, substitute your own short username (whoami will tell you).


The core idea

Claude Desktop is an Electron app. Electron decides where to store its profile — cookies, login tokens, settings — from a directory it calls userData. Two copies pointed at the same userData share a login; point them at different directories and they’re fully independent.

Digging through the app bundle, I found Claude honors an environment variable for exactly this:

if (process.env.CLAUDE_USER_DATA_DIR) {
  app.setPath("userData", process.env.CLAUDE_USER_DATA_DIR);
  app.setPath("logs", resolve(process.env.CLAUDE_USER_DATA_DIR, "Logs"));
}

So the plan is:

  1. Duplicate Claude.appClaude Work.app.
  2. Bake CLAUDE_USER_DATA_DIR into the copy so it uses a separate profile folder.
  3. Give it its own bundle identifier and name.
  4. Re-sign it (editing the app breaks Apple’s signature, and macOS won’t launch an unsigned bundle).

A different userData directory also means Electron’s single-instance lock is keyed differently, so both apps run at the same time. That’s the whole trick.


Step 1 — Duplicate the app

cd /Applications
ditto "Claude.app" "Claude Work.app"

ditto (rather than cp) preserves the bundle structure and extended attributes cleanly.

Step 2 — Point the copy at a separate profile

macOS lets you set environment variables for an app right in its Info.plist via the LSEnvironment key, and LaunchServices applies them whenever the app is opened from Finder, the Dock, or open. No wrapper script needed.

APP="/Applications/Claude Work.app"
PL="$APP/Contents/Info.plist"
PB=/usr/libexec/PlistBuddy
PROFILE="$HOME/Library/Application Support/Claude-Work"

# Distinct identity
"$PB" -c "Set :CFBundleIdentifier com.anthropic.claudefordesktop.work" "$PL"
"$PB" -c "Set :CFBundleDisplayName Claude Work" "$PL"

# Point it at a separate profile directory
"$PB" -c "Add :LSEnvironment dict" "$PL"
"$PB" -c "Add :LSEnvironment:CLAUDE_USER_DATA_DIR string $PROFILE" "$PL"

The profile folder is created automatically on first launch. Your personal Claude keeps using ~/Library/Application Support/Claude; the work copy gets ~/Library/Application Support/Claude-Work.

Dead end #1: don’t rename CFBundleName

My first instinct was to also set CFBundleName to “Claude Work”. That made the app crash instantly on launch. Running the binary from Terminal gave the real reason:

FATAL:electron_main_delegate_mac.mm:66 Unable to find helper app

Electron derives the name of its bundled helper apps (Claude Helper.app, Claude Helper (Renderer).app, etc.) from CFBundleName. Rename it and Electron goes looking for Claude Work Helper.app, which doesn’t exist.

Leave CFBundleName as Claude. The visible name of your app comes from the .app filename plus CFBundleDisplayName, both of which already say “Claude Work” — so Finder, the Dock, and Spotlight all label it correctly anyway. (One cosmetic side effect: the macOS menu-bar app menu may still read “Claude”. I can live with that.)

Step 3 — Re-sign the copy

The moment you edit Info.plist, Apple’s original code signature is invalid, and on Apple Silicon macOS refuses to launch a broken-signature bundle. The fix is an ad-hoc signature (the - identity signs locally with no certificate). The one wrinkle: a bare codesign --sign - throws away the app’s entitlements, and Claude needs some of them — most importantly com.apple.security.virtualization, without which the Cowork feature refuses to run (that one cost me a while to track down — dead end #3). So the right move is to copy the entitlements off the untouched Claude.app and sign the copy with them:

# Pull the real entitlements from the pristine Claude.app
codesign -d --entitlements - --xml "/Applications/Claude.app" \
  | plutil -convert xml1 -o /tmp/claude-ent.plist -

# Drop the three team-bound keys an ad-hoc signature can't honor anyway
for k in com.apple.application-identifier \
         com.apple.developer.team-identifier \
         keychain-access-groups; do
  /usr/libexec/PlistBuddy -c "Delete :$k" /tmp/claude-ent.plist 2>/dev/null || true
done

# Ad-hoc re-sign, carrying the entitlements over
codesign --force --sign - --entitlements /tmp/claude-ent.plist "/Applications/Claude Work.app"
codesign --verify "/Applications/Claude Work.app" && echo "OK"

Dead end #2: the SIGTRAP crash and hardened runtime

Before I landed on the simple command above, I tried codesign --force --deep --sign - and the app crashed deep inside V8 with EXC_BREAKPOINT (SIGTRAP) during startup. The cause is worth understanding:

Claude ships with hardened runtime enabled and a com.apple.security.cs.allow-jit entitlement — Electron’s JavaScript engine needs JIT, and under hardened runtime JIT is only allowed with that entitlement. My deep re-sign stripped the entitlement while keeping hardened runtime on → V8 tried to JIT, got denied, and trapped.

A plain ad-hoc sign (no --deep, which in turn doesn’t re-assert hardened runtime) sidesteps this entirely: with hardened runtime off, JIT is allowed by default and no special entitlement is required. That’s why the Step 3 re-sign skips --deep. Carrying the entitlements over the way we do there doesn’t turn hardened runtime back on — only --options runtime would — so V8 stays happy.

The trade-off: ad-hoc signing means a few OS integrations that depend on a real Team ID won’t work in the copy — hardware passkeys / WebAuthn security keys and Microsoft-SSO keychain features, specifically. Those are exactly the three team-bound entitlement keys I strip before re-signing (application-identifier, team-identifier, keychain-access-groups); they can’t be honored without a real certificate, so there’s no point carrying them. Ordinary email/password and Google sign-in work fine, which is all I needed for the work account.

Dead end #3: Cowork and the virtualization entitlement

For a good while I ran the simplest possible re-sign — codesign --force --sign -, no entitlements at all — and everything I used worked. Then Anthropic shipped Cowork, and in the work copy it refused to open, throwing up “Claude’s installation appears to be corrupted. Reinstall Claude to use Cowork.” My personal Claude was perfectly fine.

Reinstalling does nothing, of course — the copy isn’t corrupted, it’s under-entitled. Cowork runs its agent inside a macOS Virtualization-framework sandbox, so before it launches it inspects the app’s own signature for the com.apple.security.virtualization entitlement. The pristine Claude.app ships with it; my bare ad-hoc re-sign had stripped it (along with every other entitlement), so Cowork saw it missing and bailed out behind that misleading “corrupted” message.

The fix is exactly the entitlement-preserving sign in Step 3. Copying the entitlements across restores com.apple.security.virtualization, and — the part I wasn’t sure would hold — Apple Silicon honors that entitlement even under an ad-hoc signature, so Cowork’s VM boots without a real Developer ID behind it. If you ever want to confirm it survived a rebuild:

codesign -d --entitlements - "/Applications/Claude Work.app" | grep virtualization

Step 4 — Launch and sign in

open "/Applications/Claude Work.app"

It opens to a fresh login screen. Sign in with your work account. From now on it runs alongside your personal Claude, each remembering its own session.

You can confirm it’s really using the separate profile:

# The running process should have the env var set…
ps eww "$(pgrep -f 'Claude Work.app/Contents/MacOS/Claude' | head -1)" | grep CLAUDE_USER_DATA_DIR

# …and files should be updating under the work profile, not the personal one.
ls -lat "$HOME/Library/Application Support/Claude-Work" | head

Giving it its own icon

Two identical coral icons in the Dock defeats the purpose. I made a distinguishing version: the same Claude burst with a small navy briefcase badge in the corner. It reads as “work” up close, and at Dock/menu-bar size it’s just a distinct blue dot — enough to tell the two apart at a glance.

macOS has all the tooling built in (sips, iconutil) plus Python’s Pillow for the compositing. Extract the current icon, composite a badge, and rebuild a multi-resolution .icns:

mkdir -p /tmp/iconwork && cd /tmp/iconwork
sips -s format png "/Applications/Claude Work.app/Contents/Resources/electron.icns" --out base_1024.png

Then a short Pillow script to draw the badge onto base_1024.png (a navy circle with a white briefcase in the bottom-right corner — any simple, recognizable mark works), saved as badged_1024.png. Rebuild the iconset at every size macOS wants and convert it:

mkdir -p work.iconset
python3 - <<'PY'
from PIL import Image
img = Image.open("badged_1024.png").convert("RGBA")
for px, name in [(16,"16x16"),(32,"16x16@2x"),(32,"32x32"),(64,"32x32@2x"),
                 (128,"128x128"),(256,"128x128@2x"),(256,"256x256"),
                 (512,"256x256@2x"),(512,"512x512"),(1024,"512x512@2x")]:
    img.resize((px, px), Image.LANCZOS).save(f"work.iconset/icon_{name}.png")
PY
iconutil -c icns work.iconset -o electron_work.icns

The gotcha: CFBundleIconName

Copying your new .icns over Contents/Resources/electron.icns isn’t enough on its own. Claude’s Info.plist has both CFBundleIconFile (the .icns) and CFBundleIconName, and when CFBundleIconName is present macOS prefers a compiled icon inside Assets.car — so your .icns gets ignored. Rebuilding an asset catalog needs Xcode tooling; far easier to just remove the key so macOS falls back to the .icns:

APP="/Applications/Claude Work.app"
cp electron_work.icns "$APP/Contents/Resources/electron.icns"
/usr/libexec/PlistBuddy -c "Delete :CFBundleIconName" "$APP/Contents/Info.plist"
codesign --force --sign - "$APP"

# Nudge the icon caches so the new icon shows right away
touch "$APP"
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$APP"
rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/com.apple.iconservices.store"
killall Dock

The catch: updates don’t propagate

Because Claude Work.app is a full copy, Claude’s auto-updater only ever touches the original Claude.app. The work copy silently falls behind until you rebuild it. Doing that by hand every update is exactly the kind of chore I’d forget.

So I wrapped the whole thing into a rebuild script, then had macOS run it for me automatically.

The rebuild script

Save this as ~/bin/refresh-claude-work.command and chmod +x it. It rebuilds the work app from whatever version Claude.app currently is, re-applies the profile and icon, and reopens the app if it was running — while never touching your work login.

#!/bin/bash
set -euo pipefail

SRC="/Applications/Claude.app"
DST="/Applications/Claude Work.app"
PROFILE="$HOME/Library/Application Support/Claude-Work"
ICON="$HOME/bin/claude-work.icns"
PB=/usr/libexec/PlistBuddy

echo "Rebuilding \"$DST\" from \"$SRC\"..."
WAS_RUNNING=0
pgrep -f "Claude Work.app/Contents/MacOS/Claude" >/dev/null 2>&1 && WAS_RUNNING=1
pkill -f "Claude Work.app/Contents/MacOS/Claude" 2>/dev/null || true
sleep 1
rm -rf "$DST"
ditto "$SRC" "$DST"

PL="$DST/Contents/Info.plist"
# CFBundleName MUST stay "Claude" (Electron finds its helper apps by it).
"$PB" -c "Set :CFBundleIdentifier com.anthropic.claudefordesktop.work" "$PL"
"$PB" -c "Set :CFBundleDisplayName Claude Work" "$PL"
"$PB" -c "Delete :LSEnvironment" "$PL" 2>/dev/null || true
"$PB" -c "Add :LSEnvironment dict" "$PL"
"$PB" -c "Add :LSEnvironment:CLAUDE_USER_DATA_DIR string $PROFILE" "$PL"

if [ -f "$ICON" ]; then
  cp "$ICON" "$DST/Contents/Resources/electron.icns"
  "$PB" -c "Delete :CFBundleIconName" "$PL" 2>/dev/null || true
fi

# Carry the entitlements over from the pristine Claude.app — a bare `--sign -`
# would strip com.apple.security.virtualization and silently disable Cowork.
ENT="$(mktemp -t claude-work-ent).plist"
codesign -d --entitlements - --xml "$SRC" 2>/dev/null \
  | plutil -convert xml1 -o "$ENT" - 2>/dev/null
"$PB" -c "Delete :com.apple.application-identifier" "$ENT" 2>/dev/null || true
"$PB" -c "Delete :com.apple.developer.team-identifier" "$ENT" 2>/dev/null || true
"$PB" -c "Delete :keychain-access-groups" "$ENT" 2>/dev/null || true
if [ -s "$ENT" ]; then
  codesign --force --sign - --entitlements "$ENT" "$DST"
else
  codesign --force --sign - "$DST"   # fallback: entitlements unavailable
fi
rm -f "$ENT"

touch "$DST"
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$DST"
rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/com.apple.iconservices.store" 2>/dev/null || true
killall Dock 2>/dev/null || true

if [ "$WAS_RUNNING" = "1" ]; then
  open "$DST"; echo "Done. Reopened Claude Work.app."
else
  echo "Done. Launch it from /Applications/Claude Work.app"
fi

(Save your badged icon once at ~/bin/claude-work.icns and the script re-applies it on every rebuild.)

Making it automatic

Two more pieces turn this into set-and-forget: a small wrapper that only rebuilds when the versions actually differ, and a launchd agent that runs the wrapper whenever Claude.app changes.

Save the wrapper as ~/bin/auto-refresh-claude-work.sh (chmod +x):

#!/bin/bash
set -uo pipefail

SRC="/Applications/Claude.app"
DST="/Applications/Claude Work.app"
REFRESH="$HOME/bin/refresh-claude-work.command"
LOG="$HOME/Library/Logs/claude-work-autorefresh.log"
PB=/usr/libexec/PlistBuddy

log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG"; }
ver() { "$PB" -c "Print :CFBundleShortVersionString" "$1/Contents/Info.plist" 2>/dev/null; }

[ -d "$SRC" ] || { log "source missing; skipping"; exit 0; }

SRC_VER="$(ver "$SRC")"; DST_VER="$(ver "$DST")"
[ -n "$SRC_VER" ] && [ "$SRC_VER" = "$DST_VER" ] && exit 0   # already matched

# The bundle may still be mid-update when we're triggered — let it settle.
sleep 5
[ "$(ver "$SRC")" != "$SRC_VER" ] && { log "source still changing; deferring"; exit 0; }
codesign --verify "$SRC" >/dev/null 2>&1 || { log "source mid-update; deferring"; exit 0; }

log "version change: work='${DST_VER:-none}' personal='$SRC_VER' -> rebuilding"
if /bin/bash "$REFRESH" >> "$LOG" 2>&1; then
  log "rebuild complete; work now at $(ver "$DST")"
else
  log "rebuild FAILED (exit $?)"
fi

The version check makes it a safe no-op, and the settle-and-verify step means it never rebuilds from a half-written bundle mid-update.

Now the agent. Save this as ~/Library/LaunchAgents/com.anthropic.claudework.autorefresh.plistreplace YOURNAME with your username (LaunchAgents need absolute paths):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.anthropic.claudework.autorefresh</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/YOURNAME/bin/auto-refresh-claude-work.sh</string>
    </array>
    <!-- Catch updates that landed while you were logged out -->
    <key>RunAtLoad</key>
    <true/>
    <!-- Fire whenever Claude.app is updated (its Info.plist is rewritten) -->
    <key>WatchPaths</key>
    <array>
        <string>/Applications/Claude.app/Contents/Info.plist</string>
    </array>
    <!-- Don't rebuild-storm if the path changes several times during an update -->
    <key>ThrottleInterval</key>
    <integer>30</integer>
    <key>StandardOutPath</key>
    <string>/Users/YOURNAME/Library/Logs/claude-work-autorefresh.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/YOURNAME/Library/Logs/claude-work-autorefresh.log</string>
</dict>
</plist>

Load it into your GUI session:

plutil -lint ~/Library/LaunchAgents/com.anthropic.claudework.autorefresh.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.anthropic.claudework.autorefresh.plist

That’s it. From now on, whenever Claude auto-updates, the agent notices the version bump, waits for the update to finish, rebuilds Claude Work.app to match (profile and icon intact), and reopens it if you had it running. Everything it does is logged to ~/Library/Logs/claude-work-autorefresh.log.

Mine proved itself within minutes: Claude updated in the background from 1.22209.0 to 1.22209.3, the agent caught the mismatch, and rebuilt the work copy without me lifting a finger.

To turn it off later:

launchctl bootout "gui/$(id -u)/com.anthropic.claudework.autorefresh"

Recap

The whole setup is a few small scripts and one launch agent, and after that I genuinely never think about it. Two Claudes, two accounts, one Mac.


Share this post:

Next Post
Hello