#7346·mem0

Claude Code plugin reports "API key missing" after a successful `mem0 login` — `plugin_sync` only updates pre-existing entries

Author: lcatlettCreated Sep 16, 2026Updated Sep 16, 2026
Labelssdk-pythonintegrationsplugin

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-plugin v0.3.1 (installed from the mem0-plugins marketplace; identical to main @ 0df3e4b)
  • Claude Code, macOS 15.6, Python 3.11
  • Key obtained via CLI OAuth login (platform.created_via set, user_email populated, file mode 0600 — the login itself is fine)

Reproduce

  1. Fresh machine: no MEM0_API_KEY in the environment, no env.MEM0_API_KEY in ~/.claude/settings.json, no export MEM0_API_KEY= in any shell rc.
  2. Install the Claude Code plugin from the marketplace without --config api_key=....
  3. Run mem0 login and complete OAuth. Confirm ~/.mem0/config.json now has a non-empty platform.api_key.
  4. 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 sideintegrations/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 sidecli/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 existing export 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_key file — 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.

  1. Add ~/.mem0/config.json as a fallback source in api_key() — make the CLI's canonical state readable by the plugin directly. This also survives plugin reinstalls, which a cached key file does not.

  2. Let sync_api_key() bootstrap, not just rotate — on login, write data_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 and settings.json; it's the wrong default for the plugin's own store.

  3. Make the no-op visible — have mem0 login report 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.