Skip to content

feat: package Recall as a native Claude Code plugin#246

Merged
edheltzel merged 1 commit into
mainfrom
fm/recall-claude-plugin-package-p2
Jul 22, 2026
Merged

feat: package Recall as a native Claude Code plugin#246
edheltzel merged 1 commit into
mainfrom
fm/recall-claude-plugin-package-p2

Conversation

@edheltzel

@edheltzel edheltzel commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Packages Recall as a native Claude Code plugin, matching the Codex bundle that landed in #245. Recall already worked on Claude; what was missing was a single-manifest package.

No existing issue tracks this (searched open + closed via gh-axi search issues).

What ships

  • .claude-plugin/marketplace.json + the plugins/recall-claude/ bundle: nine recall-* skills and the recall-memory MCP server behind one manifest.
  • scripts/build-claude-plugin.ts regenerates the skill payload from canonical agent-skills/; a drift test fails if the checked-in copies diverge.
  • docs/CLAUDE_INTEGRATION.md, written to the CODEX_INTEGRATION.md standard.

Both manifests pass claude plugin validate --strict against Claude Code 2.1.217, verified against the live CLI and the official plugin/marketplace reference rather than from memory.

Reuses the Codex seams

Same bundle shape, same generate-then-check-in payload, same isolated e2e contract. Plugin detection was added to the existing src/hosts/claude.ts host adapter rather than a new abstraction. No regression to the host decoupling.

Skills are byte-verbatim copies of agent-skills/, not adapters: those sources are already authored against Claude's own frontmatter, so rewriting them would change behavior for users migrating off the lifecycle install. Copied rather than symlinked, because Claude drops symlinks that leave the plugin root on local-path installs and core.symlinks=false checkouts would degrade the payload silently.

Migration is the substance of this PR

Claude namespaces plugin components. The plugin registers as plugin:recall:recall-memory and does not override a user-scope recall-memory — verified empirically. Claude collapses the two only when they resolve to an identical command and env, and install.sh writes bun run <path> plus an env block. So every real existing install ends up exposing the same nine tools twice from two processes, plus a doubled skill surface.

install.sh and update.sh now reconcile that, idempotently:

  • Remove only Recall-owned skill symlinks (targets resolving into shared/skills/). Real files, user-authored skills, and other tools' links are untouched; a skill dir is removed only when already empty.
  • Remove the duplicate user-scope MCP registration.
  • Leave hooks and canonicals alone.

A registration pinned to a non-default database is kept, not removed — deleting it would silently repoint memory at the default file and the user's history would read as empty. Those installs keep both surfaces by design; the doc gives the explicit manual collapse step rather than advice that no-ops.

recall doctor gains a Claude native plugin check, and its skill-symlink probes stand down once the plugin owns that surface so a converged install no longer reports as broken.

Deliberately excluded: hooks

Plugin hooks merge with settings.json hooks rather than replacing them, so bundling Stop/SessionStart/PreCompact would double every capture for anyone who also ran install.sh. Lifecycle capture stays installer-owned. This is a documented boundary, not an omission.

Isolation evidence

bun run test:e2e:claude-plugin runs entirely in a disposable HOME and a disposable RECALL_DB_PATH, and asserts before/after that both the production database and the real ~/.claude were unchanged:

isolation.test_db=/var/folders/.../recall-claude-plugin-e2e-5ANKdJ/recall-test.db
isolation.production_db=/Users/ed/.agents/Recall/recall.db
isolation.test_claude_home=/var/folders/.../home/.claude
legacy.skill_links=9
claude.version=2.1.217 (Claude Code)
claude.plugin_skills=9
claude.duplicate_before_migration=true
migration.legacy_skill_links_removed=true
migration.duplicate_mcp_removed=true
migration.user_skill_preserved=true
migration.idempotent=true
migration.custom_db_registration_preserved=true
mcp.tools_verified=9
isolation.production_db_unchanged=true
isolation.production_claude_home_unchanged=true
e2e.status=PASS

The legacy state is seeded by the installer's own _recall_link_skills_to, so migration is tested against the symlink format production actually produces instead of a hand-rolled approximation.

Independently confirmed afterwards: the real ~/.claude has no recall-marketplace, no plugin entry, and its nine legacy skill symlinks are intact.

Named behavior differences vs the Codex package

Full table in the doc. The notable one, since identical SKILL.md bytes do not imply identical behavior: Codex strips disable-model-invocation and substitutes agents/openai.yaml because it cannot interpret the field; the Claude bundle keeps it, because it is Claude's own contract.

Honestly bounded: end-to-end enforcement of that field for plugin-shipped skills is unverified. Confirming it needs an authenticated session, and I would not authenticate against the captain's live ~/.claude. A negative control also showed claude plugin validate --strict does not inspect skill frontmatter at all, so passing validation is not evidence. Named in the doc's Current Boundaries rather than assumed away.

Verification

  • bun test — 1256 pass, 0 fail
  • bun run lint — clean
  • bun run test:e2e:claude-plugin — PASS
  • bash -n on all four lifecycle scripts

Out of scope: Pi packaging, which a parallel task owns. Its behavior is unchanged here.

Issues

This PR closes nothing, deliberately - no closing keyword is used.

Re-checked against every issue in this repo (125 total, 31 open) and against each open issue's acceptance criteria, not just its title. No open issue tracks native Claude plugin packaging, and nothing in this diff fully satisfies an existing issue. A wrong Fixes would silently close still-open work, so the adjacent issues are referenced without a keyword instead.

Base branch is main (the repo default), so any keyword added here would fire on merge - which is exactly why none is present.

Related, but NOT closed

Considered and deliberately NOT linked

Attribution note

#48 (Refactor conversation imports behind an internal source-adapter registry) was resolved by #245, via src/hosts/session-source.ts + src/hosts/session-sources.ts and the src/lib/import.ts rewiring in that PR. It is not re-claimed here.

Adds `.claude-plugin/marketplace.json` and the `plugins/recall-claude/`
bundle, so Claude Code installs the nine `recall-*` skills and the
`recall-memory` MCP server from one manifest instead of user-level
configuration. Both manifests pass `claude plugin validate --strict`
against Claude Code 2.1.217.

Reuses the seams from the Codex package (#245) rather than re-inventing
them: same bundle shape, same generate-then-check-in skill payload, same
isolated e2e contract. Skills are byte-verbatim copies of `agent-skills/`
because those sources are already authored against Claude's own
frontmatter; rewriting them would change behavior for users migrating
off the lifecycle install.

Hooks are deliberately not in the bundle. Claude merges plugin hooks with
settings.json hooks rather than replacing them, so shipping lifecycle
hooks would double every capture for anyone who also ran install.sh.
Auto-capture stays installer-owned.

Migration matters because Claude namespaces plugin components: the plugin
registers as `plugin:recall:recall-memory` and does NOT override a
user-scope `recall-memory`. Claude collapses the two only when they
resolve to an identical command and env, and install.sh writes
`bun run <path>` plus an env block, so every real existing install ends
up exposing the same nine tools twice from two processes, plus a doubled
skill surface. install.sh and update.sh now reconcile that idempotently,
removing only Recall-owned symlinks and registrations. A registration
pinned to a non-default database is kept, not removed, so migration can
never silently repoint memory at the default file.

`recall doctor` reports the plugin state and flags an un-reconciled
install, and its skill-symlink probes stand down once the plugin owns
that surface so a converged install no longer reports as broken.

Verified with `bun run test:e2e:claude-plugin`, which installs the plugin
into a disposable HOME with a disposable RECALL_DB_PATH, seeds a legacy
install, proves the duplicates are removed twice over without touching a
user-authored skill, exercises all nine MCP tools, and asserts the
production database and the real ~/.claude were unchanged.
@edheltzel edheltzel self-assigned this Jul 22, 2026
@edheltzel

Copy link
Copy Markdown
Owner Author

@Atlas-Key the PR is good but it doesnt outline the correct issues to be closed.

@edheltzel edheltzel assigned Atlas-Key and unassigned edheltzel Jul 22, 2026
@edheltzel
edheltzel merged commit 73c79ab into main Jul 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants