Claude Code & Codex plugin
This repo ships a packaged plugin — cortadel-memory — under
cortadel-plugin/,
built once from a single metadata source (packaging/plugin.metadata.json)
and generated for two hosts:
| Host | What you get |
|---|---|
| Claude Code | Full: three hooks (push-recall, session bootstrap, auto-capture) + an inline MCP server (search_memory, add_memories, …) + the cortadel skill. |
| Codex | Skills only. Codex’s plugin format has no user-config templating (no ${user_config.*} substitution), so it cannot express a configurable base URL or carry your API key — the MCP server and hooks are Claude-Code-only. Wire Codex to the MCP endpoint manually per MCP integration, using codex as the {clientName} segment: <base_url>/mcp/codex. |
The plugin is zero-dependency Node (18+ built-in fetch, ESM, no build step) and
enabled by default once installed — see Data flow & privacy before you
point it at a server.
Install
Section titled “Install”Claude Code — via marketplace (recommended)
Section titled “Claude Code — via marketplace (recommended)”/plugin marketplace add cortadel/cortadel/plugin install cortadel-memory@cortadelThis reads the root .claude-plugin/marketplace.json,
which points at cortadel-plugin. On install, Claude Code prompts for the four
configuration values below and stores them (the api_key value goes to secure storage, since the
option is marked sensitive).
Claude Code — trial run / dev flow, no install
Section titled “Claude Code — trial run / dev flow, no install”claude --plugin-dir <repo>/cortadel-pluginThis skips the marketplace userConfig prompt entirely, so configure via the CORTADEL_*
environment variables instead (see Configuration — every option has both a
user_config name and a CORTADEL_* env var name).
Codex — skills only
Section titled “Codex — skills only”Codex discovers the same repo at its own marketplace path,
.agents/plugins/marketplace.json,
pointing at the same cortadel-plugin directory. Its manifest
(cortadel-plugin/.codex-plugin/plugin.json) declares only the cortadel skill — no
hooks, no MCP server, no config prompt. Install it the way your Codex client documents installing
a skill-only plugin from a marketplace entry.
Configuration
Section titled “Configuration”Four values, declared once in packaging/plugin.metadata.json and generated into both the
Claude Code userConfig schema and this table:
| Option | Required | Sensitive | Default | Meaning |
|---|---|---|---|---|
base_url |
yes | no | https://app.cortadel.ai |
Base URL the hooks use for their REST calls. No trailing slash. The inline MCP server does not follow this — its URL is pinned literal; see Hosted vs self-hosted. |
user_id |
yes | no | — | The user id your API key was minted for. Used by the hooks in their REST payloads — it is not part of the MCP URL, which carries no user segment. Set it to the key’s user: the server overrides a mismatch either way — 403 on a query-string id, a silent rescope in a request body — so a wrong value fails quietly. See Authentication. |
api_key |
yes | yes | — | API key for your user. Hosted (https://app.cortadel.ai): the dashboard there issues keys. Self-hosted: mint one on the server: dotnet Cortadel.Api.dll mint-key <user> (in Docker: docker exec <container> dotnet Cortadel.Api.dll mint-key <user>). |
client_name |
no | no | claude |
The app_name the UserPromptSubmit hook sends on its search requests, which the server uses for access logging only. It does not filter results and is not recorded on memories the hooks capture — see MCP tool naming. The MCP endpoint’s own {clientName} segment is fixed at claude in the pinned URL. |
Hosted vs self-hosted
Section titled “Hosted vs self-hosted”-
Hosted (default) — leave
base_urlathttps://app.cortadel.ai, the live Cortadel service. Get an API key from its dashboard. Hooks and MCP both talk to it; nothing else to do. -
Self-hosted — set
base_urlto your own server’s origin, e.g.http://localhost:3001for a localdocker compose up(see Self-hosting), no trailing slash.base_urlmoves the hooks only. The inline MCP server stays pinned to the hosted URL for the Desktop/claude.ai reason above, so a self-hostedbase_urlalone leaves hooks and MCP writing to different servers. Add your own MCP server alongside it:Terminal window claude mcp add --transport http cortadel-local http://localhost:3001/mcp/claude --header "Authorization: Bearer <your-key>"/plugin→ doctor warns whenbase_urland the pinned MCP URL disagree, so this is not silent.
Two ways to set these
Section titled “Two ways to set these”- Installed from the marketplace — Claude Code resolves
userConfigand injects each value into every hook process asCLAUDE_PLUGIN_OPTION_<KEY>(e.g.CLAUDE_PLUGIN_OPTION_BASE_URL). This tier always wins. --plugin-dir/ manual — set the matchingCORTADEL_*environment variable (CORTADEL_URL,CORTADEL_USER_ID,CORTADEL_API_KEY,CORTADEL_CLIENT_NAME). Read only when theCLAUDE_PLUGIN_OPTION_*name is unset, per option — you can mix both (e.g. install from the marketplace forbase_url/user_id/api_keybut still overrideclient_namevia env var).
cortadel-plugin/scripts/lib.mjs’s cfg() is the single place this resolution happens;
see that file’s readOption() for the exact precedence.
Seven more environment-variable-only options exist beyond the four userConfig fields above
(CORTADEL_RECALL_TOPK, CORTADEL_RECALL_MIN_SCORE, CORTADEL_MIN_PROMPT_CHARS,
CORTADEL_RECALL_RERANK, CORTADEL_CAPTURE_MAX_CHARS, CORTADEL_HOOKS_DISABLE,
CORTADEL_HOOKS_LOG) — see
cortadel-plugin/README.md
for the full table.
Hook behaviour
Section titled “Hook behaviour”| Hook | What it does | Budget |
|---|---|---|
UserPromptSubmit |
Push-recall: searches Cortadel with your prompt (hybrid mode) and injects the top hits as context on every prompt. | 12 s request budget inside a 15 s hook timeout. |
SessionStart |
Bootstrap: injects a short “memory is active” notice plus your most recent memories. | 7 s budget / 10 s timeout. |
Stop |
Auto-capture: sends the last user→assistant exchange of the session transcript to POST /api/v1/memories/from-conversation for fact extraction. Runs async — never blocks the UI. |
110 s budget / 120 s timeout. |
Everything fails open: missing config, server errors, timeouts, or malformed input all exit 0
with no output. The plugin can slow a prompt down by at most its request budget; it can never
break a Claude Code session.
Data flow & privacy
Section titled “Data flow & privacy”The plugin is enabled by default as soon as it’s installed and configured — there is no
opt-in step beyond installing it. Concretely, once base_url/user_id/api_key are set:
- Every prompt you type is sent (first 4000 chars) to your configured
base_urlas a search query, on theUserPromptSubmithook. - Every final exchange of a session (the last user + assistant turn) is sent to your
configured
base_urlfor fact extraction, on theStophook.
Point the plugin only at a base_url you trust with that content. The default is the hosted
Cortadel service at https://app.cortadel.ai; pointing base_url at your own self-hosted instance
instead keeps this content on infrastructure you control. Either way, the plugin itself does not
filter or redact anything before sending it.
Off switch
Section titled “Off switch”Set CORTADEL_HOOKS_DISABLE=1 (env var, not a userConfig option — see
Configuration) to silence all three hooks without uninstalling the plugin. The
MCP server (if you’ve also wired it into an agent) is unaffected — only the automatic hooks are
gated by this variable. To stop everything, disable or uninstall the plugin via /plugin.
MCP tool naming
Section titled “MCP tool naming”The inline MCP server is named cortadel in mcpServers (visible as mcp__cortadel__<tool> in
tool-use output) and exposes the two Cortadel MCP tools, add_memories and search_memory. Its
URL is a literal, not a template:
https://app.cortadel.ai/mcp/claudeIt is deliberately not templated from base_url. Claude Desktop and claude.ai consume this same
plugin.json, but they perform no ${user_config.*} substitution — they copy mcpServers[].url
verbatim into the connector dialog, which validates it with startsWith("https"). A templated URL
begins with $ and fails there as “URL must start with ‘https’”, which is exactly the bug this
pinning fixes. Self-hosting? See Hosted vs self-hosted.
client_name is what the server sees as the calling app’s name — the {clientName} path
segment (see MCP integration), and the app_name field the UserPromptSubmit hook
sends on its search requests, where the spec defines it as “application name for access logging”.
It does not filter search results, and it is not stamped on memories the hooks capture:
the capture endpoint (POST /api/v1/memories/from-conversation) has no app_name field at all,
so app_name on a stored memory reflects whatever wrote it — the MCP server, an SDK, or the
dashboard.
Troubleshooting
Section titled “Troubleshooting”The hooks are deliberately silent on failure, so “nothing happens” is the main failure mode. Start with the bundled diagnostic, which resolves the same config the hooks use, checks health, auth, and a live read, and reports every check rather than stopping at the first failure:
node "<plugin-dir>/scripts/doctor.mjs" # or run the /doctor skill inside Claude CodeIt exits 0 when everything passes, 1 otherwise, and never prints your API key. Crucially it reads
the config Claude Code persisted on disk as well as the environment — a marketplace install
resolves nothing from the environment of an ordinary shell, because CLAUDE_PLUGIN_OPTION_*
is injected into hook subprocesses only.
- 401 — missing/invalid API key.
- 403 — the key is valid but
user_iddoesn’t match the user the key was minted for. - No memories injected — prompt too short (
CORTADEL_MIN_PROMPT_CHARS, default 10), a slash/!command, an empty search result, everything belowCORTADEL_RECALL_MIN_SCORE, or the request exceeded its budget. - Stop captures “nothing” — tool-only or trivial (<80 chars combined) exchanges are skipped;
{"no_facts_extracted":true}from the server is a normal outcome, not an error. Conversations about tooling, debugging sessions, and facts already in the store all legitimately extract nothing — a healthy install can genuinely capture zero facts for a whole session. - “Is it even running?” — set
CORTADEL_HOOKS_LOGto a file path. Each hook then appends one JSON line per invocation recording its outcome (stored/no-facts/error/skip, with the status code or guard reason), which is the only way to tell “the extractor found nothing” apart from “the server said 401” — both are silent otherwise. It records outcomes and counts only, never prompt text, memory content, or the key. claude plugin validatefails after editing the plugin — you likely hand-edited a generated file.cortadel-plugin/.claude-plugin/plugin.json,cortadel-plugin/.codex-plugin/plugin.json,.claude-plugin/marketplace.json, and.agents/plugins/marketplace.jsonare all generated bynode packaging/generate.mjsfrompackaging/plugin.metadata.json— edit the source and regenerate, never the output.- Upgraded and hooks stopped finding a server — check stderr for a
[cortadel-memory] MEMFORGE_* is set, but ...diagnostic; the plugin’s env vars were renamed fromMEMFORGE_*toCORTADEL_*with no backward-compatible fallback.
See also cortadel-plugin/README.md
for the full environment-variable reference and MCP integration for the underlying
endpoint contract.