brandonwie.dev
EN / KR
On this page
devops devopsclaude-codemcpserenadual-profilecperscworktransferable

Serena MCP — Multi-Profile Setup for Claude Code (cpers/cwork)

Installing the Serena MCP server across a Claude Code dual-profile setup (cpers/cwork) plus Codex, including the four recommended hooks, the system-prompt override, and the non-obvious "installer writes to default ~/.claude.json, misses profile-specific stores" trap.

Updated May 6, 2026 8 min read

This is the full procedure for installing the Serena language-server-backed MCP server across a Claude Code dual-profile setup (cpers / cwork) plus Codex, including the four recommended hooks and the non-obvious “installer writes to default ~/.claude.json, misses profile-specific stores” trap that bites multi-profile users on first install.

Why Serena needs more than the default install

Recent Claude Code (and Opus 4.7) updates added very long built-in tool descriptions (~16k tokens, unmodifiable) and shipped a default system prompt that biases the agent toward Read / Grep over MCP-provided symbolic tools. Serena counteracts this with three mechanisms:

  1. An MCP server providing symbolic code-intelligence tools backed by language servers (get_symbols_overview, find_symbol, find_referencing_symbols, replace_symbol_body, etc.).
  2. Four behavioral hooks (activate, remind, auto-approve, cleanup) that nudge the agent away from the default bias.
  3. A system-prompt override (~7800 chars) that re-anchors the agent toward symbolic-first workflows.

The serena setup claude-code CLI handles step 1 only. Steps 2 and 3 are manual. And in a dual-profile setup (cpers runs Claude with CLAUDE_CONFIG_DIR=~/.claude, cwork with CLAUDE_CONFIG_DIR=~/.claude-work), even step 1 needs to be re-run per profile because the installer ignores the env var.

Difficulties Encountered

1. The Dual-Profile Installer Trap

serena setup claude-code is a thin wrapper around:

claude mcp add --scope user serena -- serena start-mcp-server --context=claude-code --project-from-cwd

claude mcp add --scope user writes to $HOME/.claude.json unconditionally — NOT to whichever path CLAUDE_CONFIG_DIR resolves to. So in a dual-profile setup:

~/.claude.json              ← installer writes here (default home store)
~/.claude/.claude.json      ← cpers reads here (CLAUDE_CONFIG_DIR=~/.claude)
~/.claude-work/.claude.json ← cwork reads here (CLAUDE_CONFIG_DIR=~/.claude-work)

After serena setup claude-code, both profile stores are still missing the serena entry. /mcp shows N other servers but no serena. Plain claude (which WOULD have read the orphan entry) is disabled by the zshrc launcher gate. The serena MCP entry sits in a file no profile reads.

Fix:

CLAUDE_CONFIG_DIR=~/.claude      serena setup claude-code
CLAUDE_CONFIG_DIR=~/.claude-work serena setup claude-code

This re-runs the installer twice with the env override, landing entries in both profile-specific stores. The orphan in ~/.claude.json can be left in place; plain claude is gated off and the file holds many other unrelated state keys (OAuth, growthbook flags, etc.) that should not be hand-edited.

2. --system-prompt vs --append-system-prompt

Serena’s docs example shows:

claude --system-prompt="$(serena prompts print-cc-system-prompt-override)"

A common (incorrect) assumption: --system-prompt is --print-mode-only, forcing interactive launchers to use --append-system-prompt instead. Wrong — both flags work in interactive Claude Code (verified via claude --help). The substantive difference:

FlagBehavior
--system-promptREPLACES the default system prompt entirely
--append-system-promptAPPENDS to the default system prompt

Serena’s override is ~7800 chars beginning with "You are Claude Code, Anthropic's official CLI for Claude..." — written as a replacement. Using --append- would stack two "You are Claude Code..." preambles redundantly and dilute the override’s signal. Use --system-prompt.

Both ~/.claude/settings.json and ~/.claude-work/settings.json are supposed to symlink to 3b/.agents/global-claude-setup/settings.json (the SoT). In practice, Claude Code’s UI atomically rewrites these under load (create-temp-then-rename), silently replacing the symlink inode with a regular file. After install of new hooks in SoT, profile copies stay stale until the symlinks are restored via /check-symlinks (or ln -sf {SoT} ~/.claude/settings.json). Same bug class that defeated claude-mem removal attempts #1 and #2.

4. Codex MCP Worked Out of the Box

serena setup codex writes to ~/.codex/config.toml, which is symlinked to 3b/.agents/global-codex-setup/config.toml. Single profile, no cpers/cwork analog → no installer trap. The MCP entry just works. Hook setup remains manual.

The full install sequence

Step 1 — MCP entry per Claude profile

# Personal profile
CLAUDE_CONFIG_DIR=~/.claude      serena setup claude-code
# Work profile
CLAUDE_CONFIG_DIR=~/.claude-work serena setup claude-code

Verify both ~/.claude/.claude.json and ~/.claude-work/.claude.json contain:

"serena": {
  "type": "stdio",
  "command": "serena",
  "args": ["start-mcp-server", "--context=claude-code", "--project-from-cwd"],
  "env": {}
}

Codex MCP entry is one-shot: serena setup codex (writes to ~/.codex/config.toml — already symlinked into 3B SoT in this setup).

Step 2 — Four hooks in Claude SoT settings.json

Edit 3b/.agents/global-claude-setup/settings.json (the symlinked SoT):

"SessionStart": [
  { "matcher": "", "hooks": [{ "type": "command", "command": "serena-hooks activate --client=claude-code" }] }
],
"PreToolUse": [
  { "matcher": "",                "hooks": [{ "type": "command", "command": "serena-hooks remind --client=claude-code" }] },
  { "matcher": "mcp__serena__*",  "hooks": [{ "type": "command", "command": "serena-hooks auto-approve --client=claude-code" }] }
],
"Stop": [
  { "matcher": "", "hooks": [{ "type": "command", "command": "serena-hooks cleanup --client=claude-code" }] }
]

Hook semantics (from Serena docs):

HookEventEffect
activateSessionStartPrompts agent to activate the project at session start, reads Serena instructions.
remindPreToolUse ("")Throttled internally — nudges agent toward Serena tools after consecutive non-Serena tool calls (Read/Grep/etc.).
auto-approvePreToolUse (mcp__serena__*)Auto-approves Serena calls when CC is in acceptEdits mode (covers destructive ones like replace_symbol_body, rename_symbol).
cleanupStopCleans hook session state on session end.

Step 3 — One hook in Codex SoT hooks.json

Edit 3b/.agents/global-codex-setup/hooks.json:

"SessionStart": [
  { "hooks": [{ "type": "command", "command": "serena-hooks activate --client=codex" }] }
]

Codex docs explicitly recommend SessionStart only — the Codex hook system is “less refined” and Codex shows less drift natively. auto-approve does not apply (no acceptEdits analog). remind/cleanup are not endorsed by docs; add later if drift surfaces.

# /check-symlinks skill in Claude Code, or manually:
ln -sf /Users/brandonwie/dev/personal/3b/.agents/global-claude-setup/settings.json /Users/brandonwie/.claude/settings.json
ln -sf /Users/brandonwie/.claude/settings.json /Users/brandonwie/.claude-work/settings.json

Work profile chains through personal (per setup.sh line 241+) so a single SoT edit propagates to both.

Step 5 — System-prompt override in shell launchers

In dotfiles/zsh/.zshrc (or wherever cpers/cwork are defined):

# Lazy cache — one subprocess per shell session.
function _serena_cc_prompt() {
  if [[ -z "${_SERENA_CC_PROMPT_CACHE-}" ]]; then
    _SERENA_CC_PROMPT_CACHE=$(serena prompts print-cc-system-prompt-override 2>/dev/null)
  fi
  printf '%s' "${_SERENA_CC_PROMPT_CACHE}"
}

function cwork() {
  >&2 printf '[claude] profile=work dir=%s cwd=%s
' "${HOME}/.claude-work" "$PWD"
  CLAUDE_CONFIG_DIR=~/.claude-work command claude --system-prompt="$(_serena_cc_prompt)" "$@"
}

function cpers() {
  >&2 printf '[claude] profile=personal dir=%s cwd=%s
' "${HOME}/.claude" "$PWD"
  CLAUDE_CONFIG_DIR=~/.claude command claude --system-prompt="$(_serena_cc_prompt)" "$@"
}

Graceful degradation: if serena is missing/broken, _serena_cc_prompt returns empty → --system-prompt="" (no-op). The launcher still works without serena installed.

Step 6 — Verify

# 1. Binaries reachable
which serena serena-hooks
serena --version
serena-hooks --help | grep -E "activate|remind|cleanup|auto-approve"

# 2. MCP entry per profile
jq '.mcpServers.serena' ~/.claude/.claude.json
jq '.mcpServers.serena' ~/.claude-work/.claude.json
grep -A 2 '[mcp_servers.serena]' ~/.codex/config.toml

# 3. Hook count in Claude SoT (expect 4 serena-hooks entries)
jq '[.hooks.SessionStart, .hooks.PreToolUse, .hooks.Stop] | flatten
    | map(select(.hooks[].command | contains("serena-hooks")))
    | length' 
  ~/dev/personal/3b/.agents/global-claude-setup/settings.json

# 4. Hook count in Codex SoT (expect 1 serena-hooks entry under SessionStart)
jq '.hooks.SessionStart' 
  ~/dev/personal/3b/.agents/global-codex-setup/hooks.json

# 5. Symlinks restored
ls -la ~/.claude/settings.json ~/.claude-work/settings.json

# 6. Launcher carries the flag
zsh -c 'source ~/.zshrc; which cpers' | grep system-prompt

Restart Claude Code. /mcp should now show serena · ✔ connected under the appropriate user MCP section per profile.

Why this approach over the alternatives

The five obvious approaches and the trade-offs that made the env-override + SoT path the chosen one:

ApproachProCon
Hand-edit ~/.claude/.claude.json per profileMost directThe file is also the OAuth/state store for that profile — risk of corrupting unrelated keys.
serena setup claude-code + CLAUDE_CONFIG_DIRUses upstream installer, just env-overriddenTwo installer invocations, but trivially cheap. Chosen.
Hand-edit SoT mcpServers blockSingle sourcemcpServers lives in ~/.claude/.claude.json per profile, NOT in the SoT settings.json. Doesn’t apply.
claude mcp add --scope project per repoRepo-scopedDefeats “global” intent — must remember to install per repo.
Skip auto-approve hookSimplerLoses blanket-approval coverage for destructive Serena calls in acceptEdits mode.

The chosen approach (env-override + SoT symlinks + shell-launcher injection) keeps the install global, leverages existing 3B SoT pipelines, and degrades gracefully when Serena is uninstalled.

Key takeaways for any multi-profile MCP install

  • MCP entries live in profile-specific .claude.json files, NOT in the shared SoT settings.json. The two are completely different files (one contains hooks/permissions/plugins; the other contains MCP servers + OAuth state).
  • claude mcp add --scope user ignores CLAUDE_CONFIG_DIR ONLY IF you invoke it directly without the env override. Setting the env explicitly fixes it: CLAUDE_CONFIG_DIR=~/.claude-work claude mcp add ....
  • The four-hook layout is necessary, not optional, for Opus 4.7. Without the system-prompt override + remind hook, the agent strongly prefers Read and Grep over find_symbol/get_symbols_overview — defeating the point of installing Serena.
  • Codex needs less. Single MCP entry + one SessionStart hook. No auto-approve analog. No --system-prompt override needed (Codex shows less drift natively).
  • Restart Claude Code after every settings.json or .claude.json change. In-flight sessions don’t pick up new MCP entries or new hooks.
  • Verify symlinks after every install/upgrade involving SoT files. The atomic-rename drift class (UI rewrites symlinks as regular files) breaks the SoT pipeline silently.

When this fits

This is the pattern for installing any new MCP server in a Claude Code dual-profile setup, onboarding a new machine to Serena where cpers/cwork already exist, or diagnosing “the MCP server is registered but /mcp doesn’t show it” after an installer ran successfully. Skip it for single-profile Claude Code setups (no CLAUDE_CONFIG_DIR indirection — serena setup claude-code works as-is) and for per-repo (project-scope) MCP installation, which uses a different mechanism that edits .mcp.json directly.

References

Comments

enko