Claude Code plugin reports "API key missing" after a successful `mem0 login` — `plugin_sync` only updates pre-existing entries
Summary
On a clean install, mem0 login succeeds and writes a valid key to
~/.mem0/config.json (platform.api_key), but the Claude Code plugin reports
mem0_api_key: missing / mem0_authentication: API key missing and captures
nothing. /mem0:status then advises reinstalling with --config api_key=...
— a key the user already has.
plugin_sync.sync_api_key() exists to prevent exactly this, but every one of
its targets is update-only ("never create new ones"), so on a first-time setup
it matches nothing and returns []. The failure is silent by design: the call
site swallows all exceptions and the empty return is never surfaced.
Environment
integrations/claude-code-pluginv0.3.1 (installed from themem0-pluginsmarketplace; identical tomain@0df3e4b)- Claude Code, macOS 15.6, Python 3.11
- Key obtained via CLI OAuth login (
platform.created_viaset,user_emailpopulated, file mode 0600 — the login itself is fine)
Reproduce
- Fresh machine: no
MEM0_API_KEYin the environment, noenv.MEM0_API_KEYin~/.claude/settings.json, noexport MEM0_API_KEY=in any shell rc. - Install the Claude Code plugin from the marketplace without
--config api_key=.... - Run
mem0 loginand complete OAuth. Confirm~/.mem0/config.jsonnow has a non-emptyplatform.api_key. - Run
/mem0:status.
Expected: key resolved; mem0_authentication PASS.
Actual:
FAIL mem0_api_key: missing
FAIL mem0_authentication: API key missing
with "api_key_configured": false from memory_cli.py status --json, while
events keep accumulating locally and flushes stays at 0.
Root cause
Two halves of the same repo disagree about where the key lives, and the bridge between them only handles rotation, not bootstrap.
Plugin side — integrations/claude-code-plugin/core/memory_core.py:383-396
resolves the key from exactly two sources:
def api_key() -> str:
configured = (
os.environ.get("MEM0_API_KEY")
or os.environ.get("PLUGIN_OPTION_API_KEY")
or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
or ""
).strip()
if configured:
return configured
try:
return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
except OSError:
return ""
It never reads ~/.mem0/config.json. Its own cache file is written only by
cache_plugin_api_key() (memory_core.py:399-430), which requires
PLUGIN_OPTION_API_KEY — i.e. it only ever fires if the user installed the
plugin with --config api_key=....
CLI side — cli/python/src/mem0_cli/plugin_sync.py is built for this
exact problem, but each target bails when no entry pre-exists:
_update_claude_settings()(:59-78) —if not isinstance(env, dict) or "MEM0_API_KEY" not in env: return False # No existing entry — don't create one._update_shell_rc()(:90-104) — returns False when the regex finds no existingexport MEM0_API_KEY=line.- The plugin's own key file is explicitly out of scope per the module
docstring: "Plugin's own
<plugin-dir>/.api_keyfile — plugin-managed".
So for a first-time user all three paths return False, sync_api_key()
returns [], and the caller (config.py:187-194) wraps it in
except Exception: pass. Nothing is written and nothing is reported.
Verified on my machine at login time: env.MEM0_API_KEY absent from
~/.claude/settings.json, no export MEM0_API_KEY in .zshrc/.bashrc/
.bash_profile. All three targets no-ops.
The docstring's <plugin-dir>/.api_key also doesn't match the path the plugin
actually uses — data_dir()/api-key (hyphen, no leading dot) — which suggests
the two halves have drifted.
Impact
The user does everything correctly and gets a false negative. Because capture
stays active while auth fails, events accumulate with flushes: 0, so it looks
like memory is on. Nothing surfaces the gap until someone runs doctor, and
doctor's remedy points at a reinstall rather than the actual cause.
Suggested fixes
Either alone closes it; the first is the smaller change.
Add
~/.mem0/config.jsonas a fallback source inapi_key()— make the CLI's canonical state readable by the plugin directly. This also survives plugin reinstalls, which a cached key file does not.Let
sync_api_key()bootstrap, not just rotate — on login, writedata_dir()/api-key(0600, via the existing atomic-write helper) when no other target exists. The "never create new ones" rule is right for shell rcs andsettings.json; it's the wrong default for the plugin's own store.Make the no-op visible — have
mem0 loginreport which touchpoints were synced, and warn when the key reached none of them.sync_api_key()already returns the list; the caller discards it.
Secondary: reword the 401 row in the plugin README and the /mem0:status
remedy to distinguish missing from unreadable-from-here, so the advice
isn't "reinstall with a key" when a valid key already exists on disk.
Workaround
Copy the key the CLI already has into the path the plugin expects:
python3 -c "
import json, pathlib, os
key = json.loads((pathlib.Path.home()/'.mem0/config.json').read_text())['platform']['api_key']
d = pathlib.Path.home()/'.claude/plugins/data/mem0-mem0-plugins'
fd = os.open(d/'api-key', os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0o600)
os.fdopen(fd,'w').write(key)
"
After this, all doctor checks pass and mem0_authentication connects. Note
this lives in the plugin data directory, so it may not survive a plugin
update or reinstall — and re-running mem0 login will not restore it, since
login writes to config.json instead.
Source: mem0ai/mem0