Claude Code Settings Sync and Troubleshooting
- 1. Multi-Account Claude Code via CLAUDE_CONFIG_DIR
- 2. Sharing Claude Code Sessions via Symlinked .jsonl
- 3. Claude Code Settings Sync and Troubleshooting
Running multiple accounts adds a new kind of friction: settings drift. Install a plugin in one account and it's invisible in the others. Add a slash command, update a hook, configure an MCP server — any change made in one account has to be manually replicated elsewhere. The more accounts, the worse it gets.
Seamless multi-account use requires a clear boundary: some settings should be identical across all accounts, others should stay account-specific. Drawing that line and automating the sync is what this post covers.
Ep1 covered CLAUDE_CONFIG_DIR-based account isolation; Ep2 covered session sharing. This post adds the operational layer: what to share, what to keep isolated, how to migrate safely, and how to debug common failures.
What needs syncing
Each account directory (~/.claude, ~/.claude-2, ~/.claude-3, ~/.claude-4) is a fully independent config root. By default, changes made in one account — installing a plugin, adding a slash command, updating a hook — are invisible to all others. The sync script (sync-claude.sh) addresses this through bidirectional propagation: first pulling the newest version of each shared artifact into the primary instance, then pushing the reconciled state out to every account.
The following artifacts are sync candidates:
settings.json— API keys, environment variables, hook definitions, model preferences, permission rules. Everything else degrades gracefully if it drifts; this file does not.CLAUDE.md— The user instruction document that shapes Claude's behavior across sessions. Any account where this diverges will behave differently.cwf-hooks-enabled.sh— Feature flag file for conditional hook activation. Shared so that enabling a feature in one account propagates to all.commands/— Slash command definitions. A command added via one account becomes available everywhere.hooks/— Hook scripts referenced bysettings.json. The hook definitions insettings.jsonand the scripts inhooks/must stay aligned.skills/,agents/,hud/— OMC-layer extensions. Treated as shared by default.plugins/— Handled differently: not copied but symlinked. All accounts point to~/.claude/pluginsas the physical source.
Two categories stay isolated regardless of sync configuration:
- Session history (
.jsonlfiles inprojects/) — Handled separately via the shared projects symlink described in Ep2. The sync script does not touch these. memory/— Per-account memory is independent by default. Cross-account memory sharing requires an explicit symlink step, described in the troubleshooting section below.
Share-vs-isolate matrix
| Artifact | Sync behavior | Direction | Notes |
|---|---|---|---|
settings.json |
Synced | Bidirectional | .claude-3 receives a filtered copy (see below) |
CLAUDE.md |
Synced | Bidirectional | Full copy |
cwf-hooks-enabled.sh |
Synced | Bidirectional | Full copy |
commands/ |
Synced | Bidirectional | rsync --delete in forward pass |
hooks/ |
Synced | Bidirectional | rsync --delete in forward pass |
skills/ |
Synced | Bidirectional | rsync --delete in forward pass |
agents/ |
Synced | Bidirectional | rsync --delete in forward pass |
hud/ |
Synced | Bidirectional | rsync --delete in forward pass |
plugins/ |
Symlinked | N/A | All accounts point to ~/.claude/plugins |
projects/ (sessions) |
Not touched | N/A | Managed by Ep2 symlink |
memory/ |
Isolated | N/A | Opt-in symlink only |
history.jsonl |
Isolated | N/A | Per-account, never synced |
The .claude-3 exception
.claude-3 is treated as a tracing/telemetry instance. When the forward sync pass writes settings.json to it, the script applies a jq filter that removes two categories of content before writing:
OTEL_* environment variables — the tracing instance maintains its own OpenTelemetry configuration and must not inherit the primary instance's values.
Hook commands containing sync or cctrace in their command string — these are instance-specific behaviors that should not propagate to the tracing account.
The filter applied:
jq '
.env |= with_entries(select(.key | startswith("OTEL_") | not)) |
if .hooks then
.hooks |= with_entries(
.value |= map(
.hooks |= map(select(.command | (test("sync"; "i") or test("cctrace"; "i")) | not))
)
)
else . end
' ~/.claude/settings.json > ~/.claude-3/settings.jsonThe consequence: .claude-3/settings.json is always overwritten by the forward sync pass. Any manual edit to that file will be lost the next time the script runs. If you need a persistent setting in .claude-3, make it in ~/.claude/settings.json at the source, not in the destination.
Migration steps
From single account to multi-account with sync
If you are starting from a single ~/.claude and want to add ~/.claude-2:
# 1. Create the second account directory
mkdir ~/.claude-2
# 2. Run Claude once with the new account to let it initialize
CLAUDE_CONFIG_DIR=~/.claude-2 claude --version
# 3. Run sync to propagate primary settings to the new account
bash ~/.claude/scripts/sync-claude.shThe sync script's Phase 2 (forward pass) will copy settings.json, CLAUDE.md, all command/hook/skill directories, and create the plugins symlink in ~/.claude-2. The new account starts with a full copy of your primary configuration.
Adding sync to an existing multi-account setup
If ~/.claude-2 already has its own settings.json that differs from ~/.claude:
# 1. Back up both before anything runs
cp ~/.claude/settings.json ~/.claude/settings.json.bak
cp ~/.claude-2/settings.json ~/.claude-2/settings.json.bak
# 2. Run sync — Phase 1 pulls newer files from secondary accounts
# into primary before Phase 2 pushes primary out to all accounts
bash ~/.claude/scripts/sync-claude.sh
# 3. Verify the merged result looks correct
cat ~/.claude/settings.jsonPhase 1 uses rsync --update, which copies a file from a secondary account to the primary only if the secondary's version is newer by modification timestamp. If both files have the same timestamp (common when copied manually), neither overwrites the other. In that case, manually reconcile the two settings.json files before running the script.
Rollback order
If sync produces an unexpected result:
# Restore primary from backup
cp ~/.claude/settings.json.bak ~/.claude/settings.json
# Restore secondary accounts
cp ~/.claude-2/settings.json.bak ~/.claude-2/settings.json
cp ~/.claude-3/settings.json.bak ~/.claude-3/settings.json
# Plugins symlinks: if broken, recreate manually
rm ~/.claude-2/plugins ~/.claude-3/plugins ~/.claude-4/plugins
ln -sf ~/.claude/plugins ~/.claude-2/plugins
ln -sf ~/.claude/plugins ~/.claude-3/plugins
ln -sf ~/.claude/plugins ~/.claude-4/pluginsAlways back up before running the script in an environment where settings.json files have diverged significantly. The --update flag protects against overwriting newer files with older ones, but it cannot protect against timestamp collisions.
Common failures
Broken plugins symlink
Symptom: Commands from a plugin that works in ~/.claude are unavailable in ~/.claude-2. Running ls -la ~/.claude-2/plugins shows a broken symlink or a plain directory.
Cause: The plugins path in the secondary account was replaced with a directory (e.g., by a manual cp -r), or the symlink target moved.
Fix:
rm -rf ~/.claude-2/plugins
ln -sf ~/.claude/plugins ~/.claude-2/pluginsRepeat for each affected account. The sync script recreates the symlink on every forward pass, so the next sync will also fix this automatically.
settings.json parse error
Symptom: Claude Code fails to start in an account, or starts with no hooks/permissions active. The terminal may show a JSON parse error.
Cause: A partial write left settings.json in a malformed state — common when the jq filter for .claude-3 is interrupted mid-write, or when a manual edit introduced a trailing comma or mismatched brace.
Fix:
# Validate syntax
cat ~/.claude-2/settings.json | python3 -m json.tool > /dev/null
# If invalid, restore from primary or backup
cp ~/.claude/settings.json ~/.claude-2/settings.jsonFor .claude-3, do not copy directly from primary — run the sync script instead so the filter is applied correctly.
Hook conflict between accounts
Symptom: A hook runs in the primary account but silently does nothing in a secondary, or produces unexpected behavior because the hook script path is wrong.
Cause: The settings.json hook definition references an absolute path (e.g., /Users/you/.claude/hooks/SessionEnd/sync-step-1) that exists in the primary account's hooks/ directory but was not synced to the secondary, or was synced but the settings.json still points to the primary path.
Fix: Use ~/.claude/hooks/ as the path in hook definitions if the hooks directory is synced — the secondary account will have the same scripts under ~/.claude-2/hooks/, but if settings.json hard-codes ~/.claude/, it still resolves correctly because the path exists on disk. Verify hook script permissions after sync:
ls -l ~/.claude-2/hooks/SessionEnd/
chmod +x ~/.claude-2/hooks/SessionEnd/*sessions-index.json race condition
Symptom: An external tool (such as cc-session-sync or qmd) reports incomplete or stale session data. Sessions created recently are missing from search results.
Cause: Two tools updated sessions-index.json simultaneously. The later write overwrote the earlier write's changes.
Claude Code resume is unaffected — it scans .jsonl files directly and ignores the index file. Only external tools that rely on the index are affected.
Fix:
# Delete the index; tools will regenerate it on next run
rm ~/.claude/projects/sessions-index.jsonTo prevent recurrence, stagger tool schedules in crontab so concurrent writes are unlikely:
0 */6 * * * /path/to/tool-a
30 */6 * * * /path/to/tool-bMemory isolation causing missing context
Symptom: A session resumed in ~/.claude-2 lacks bookmarks, memory entries, or context variables that were set in ~/.claude.
Cause: The memory/ directory is intentionally isolated per account. There is no automatic sync for memory.
Fix (opt-in sharing): Symlink all accounts' memory to the primary:
for ACCT in ~/.claude ~/.claude-[0-9]*; do
if [ -d "$ACCT/memory" ] && [ ! -L "$ACCT/memory" ]; then
mv "$ACCT/memory" "$ACCT/memory.bak.$(date +%s)"
ln -sf ~/.claude/memory "$ACCT/memory"
fi
doneAfter this, any write to memory from any account lands in ~/.claude/memory. If you prefer one-time copy without ongoing sharing, use rsync instead of symlink.
PROJ_KEY collision
Symptom: Two unrelated projects show each other's sessions in the resume dialog.
Cause: Claude Code encodes the project path into a key (PROJ_KEY). Paths that differ by - versus / in different positions can produce the same encoding. Example: /a/b-c and /a-b/c may collide.
Fix: This is a limitation of Claude Code's session indexing, not of the multi-account setup. Rename one of the projects to use a clearly distinct path:
# Avoid ambiguous path structures
# Prefer: ~/Projects/my-app
# Avoid: ~/Projects-old/my/appDiagnostics
Verify Claude Code version
claude --version
CLAUDE_CONFIG_DIR=~/.claude-2 claude --versionBoth should return the same binary version. If they differ, the shell function from Ep1 may be resolving to different binaries.
Check which config directory is active
echo $CLAUDE_CONFIG_DIRIf empty, Claude Code is using ~/.claude. In the shell function from Ep1, the variable is set inline for the subprocess and does not persist in the shell environment.
Validate settings.json syntax
python3 -m json.tool ~/.claude/settings.json > /dev/null && echo "OK"
python3 -m json.tool ~/.claude-2/settings.json > /dev/null && echo "OK"
python3 -m json.tool ~/.claude-3/settings.json > /dev/null && echo "OK"A non-zero exit with no "OK" output means the file contains invalid JSON.
Inspect symlink state
# plugins symlinks
file ~/.claude-2/plugins ~/.claude-3/plugins ~/.claude-4/plugins
# projects symlinks (from Ep2)
file ~/.claude/projects ~/.claude-2/projects ~/.claude-3/projectsEach should report symbolic link to ~/.claude/plugins or ~/.claude-shared/projects respectively. A line reading directory or broken symbolic link needs attention.
Check file ownership and permissions
ls -la ~/.claude/settings.json
ls -la ~/.claude-2/settings.json
ls -la ~/.claude/hooks/SessionEnd/Hook scripts must be executable (-rwxr-xr-x). Settings files must be readable by the current user. On a single-user macOS machine these rarely cause problems, but they can appear after copying files from another machine or restoring from a backup.
Locate session log files
Claude Code writes session logs to the shared projects directory. To verify where they are landing:
find ~/.claude-shared/projects -name "*.jsonl" | head -10If sessions from a particular account are not appearing, verify that account's projects symlink resolves correctly:
ls ~/.claude-2/projects | head -5Check disk usage
If disk usage has grown unexpectedly after setting up session sharing:
du -sh ~/.claude-shared/projects/
du -sh ~/.claude/projects.bak.*
du -sh ~/.claude-2/projects.bak.*Backups created by the Ep2 migration script accumulate over time. Remove backups older than 30 days when you are confident the setup is stable:
find ~/.claude ~/.claude-[0-9]* -maxdepth 1 -name "projects.bak.*" -mtime +30 -exec rm -rf {} \;Series close
Across three posts, the setup covers isolation (Ep1: CLAUDE_CONFIG_DIR separates each account into its own config root), continuity (Ep2: a symlinked projects directory gives every account visibility into the same session history), and consistency (this post: a bidirectional sync script keeps settings, hooks, commands, and plugins aligned across accounts while preserving account-specific overrides). The practical limit of this approach is that it runs on a single OS user's filesystem — cross-user sharing requires group permission configuration and falls outside the typical use case. For most developers running two to four Claude Code accounts on one machine, the combination of CLAUDE_CONFIG_DIR isolation, shared session storage, and sync-on-SessionEnd is a stable, low-maintenance operating pattern.