Repository navigation
fix(mcp): register the marketplace before the Claude Code plugin install - #1294
Merged
Merged
Conversation
`claude plugin install <name>` resolves a bare name only against marketplaces the user already registered, and the wizard never opens Claude Code interactively. Register `PostHog/ai-plugin` first, then install `posthog@posthog`, refreshing a stale catalog and falling back to the bare name. Run the CLI through `execFile` rather than `execSync`. The calls clone git repositories and take seconds; blocking the event loop froze the TUI spinner on its first frame. Read installed plugins and marketplaces from `--json`, matching plugin ids exactly, so a plugin named `posthog-something` no longer counts as ours and a copy installed from any marketplace does. Uninstall every installed `posthog@*` id, since Claude Code permits more than one and a bare uninstall removes one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Test against a Context Mill branch:
Add Results will be posted here when complete. |
`ListedPlugin` modelled `id` alone and `ListedMarketplace` modelled `name` alone, so two fields the CLI already returns went unread — and each one was carrying an answer we were guessing at instead. `plugin list --json` returns `enabled`. A plugin switched off in `/plugin` stays listed, and Claude Code serves its MCP server *from* the plugin, so a disabled plugin serves nothing. Reading `id` alone made `isPluginInstalled` report true for it: the install was skipped, the screen said the user was good to go, and no posthog server existed anywhere. Verified against the real CLI — after `claude plugin disable`, the row is still listed with `"enabled": false`. `marketplace list --json` returns `repo`. Matching on the name alone meant a marketplace merely *called* `posthog` satisfied the check, so registration was skipped and the install ran against someone else's catalog under our name. The field that distinguishes them was already in the payload. A failed `marketplace add` reached only the debug log. The three install attempts that follow then fail with `not found in any configured marketplace` — the same string the bug this branch fixes produced — so the new root cause was indistinguishable from the old one in error tracking, and from this branch's own acceptance query. The cause now travels with the install failure rather than being reported on its own, because an add that the fallback recovers from is not worth an event: the comment above `ensurePluginMarketplace` already said so, and a test already asserted it. Fixtures now emit `enabled` and `repo` as the CLI does, rather than the trimmed shape that let the first defect through. Mutation-verified: reverting each of the three fixes fails a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit taught `isPluginInstalled` to treat a disabled plugin as
not installed. That is the right answer for one caller and the wrong answer
for the other two, and the method has only one return value.
`index.ts` asks it to decide whether a client has anything to remove — its
comment says so — and `removePlugin` asks it again on the no-`--json` path.
For those, a disabled plugin is very much installed: it sits on disk and
`mcp remove` has to clear it. Answering false hid it from discovery, so the
plugin could never be removed at all.
Only the install decision cares whether the plugin actually serves. So
`isPluginInstalled` goes back to meaning present, and `installPlugin` asks
about `enabled` itself before reporting a no-op.
Verified against the real CLI, which settles what "install over a disabled
plugin" should do: it re-enables it and reports success, so falling through
to the install is the repair, not merely a retry.
disabled present=true installPlugin -> {success:true} -> enabled
absent present=false installPlugin -> {success:true} -> enabled
enabled present=true installPlugin -> {success:true,alreadyInstalled} -> unchanged
Mutation-verified in both directions: collapsing the install check back to
presence fails the disabled-install test, and moving the `enabled` filter
back into `isPluginInstalled` fails the removal test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sarahxsanders
approved these changes
Sep 22, 2026
| // throws; the caller decides what a failure means. | ||
| private runClaude(binary: string, args: string[]): Promise<ClaudeRun> { | ||
| return new Promise((resolve) => { | ||
| execFile(binary, args, (error, stdout) => { |
Collaborator
There was a problem hiding this comment.
should we add timeout/buffer here to match #1295?
`installPlugin` registers the PostHog marketplace, and `removePlugin` only uninstalled the plugin. A machine that ran `mcp add` then `mcp remove` kept our catalog forever, and `codex.ts` already clears both halves on the same call, so the two clients disagreed about what removal means. Removal now clears the marketplace too, and acts when either half is present — a marketplace left behind by a failed plugin step is a state the wizard created and should be able to undo. Guarded on it being ours, by repo and not merely by name, since removing a marketplace someone else registered under the name `posthog` is not ours to do. Guarded on it being registered at all, because `marketplace remove` exits non-zero on a marketplace that is absent and would report a failure for a no-op. Verified against the real CLI: `marketplace remove` cascades, taking the plugins installed from that marketplace with it, which is what removal wants and why the guard matters. Mutation-verified: dropping the marketplace step fails 2 tests, and matching on name without repo fails the foreign-marketplace test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gewenyu99
added a commit
that referenced
this pull request
Sep 23, 2026
Brings in the eight main fixes since beccc79 (#1275, #1280, #1284, #1294, #1295, #1301, #1302, #1305). One conflict: agentic detection keeps main's two-attempt retry loop from #1305 and adds A1's progress emitter to the agent it initializes. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ClaudeCodeMCPClient.installPluginrunsclaude plugin install posthog. Claude Code resolves a bare plugin name only against marketplaces the user has already registered, and it registers its catalog when it starts interactively — the wizard uses the non-interactivepluginsubcommand. On a machine that never opened Claude Code the install dies withPlugin "posthog" not found in any configured marketplace.258 events across 224 distinct users in the last 30 days (54 / 46 in the last 7), still occurring. The failure spans 29 error-tracking issue ids because
$HOMEand the resolved binary path sit inside the exception message.Changes
plugin marketplace add PostHog/ai-plugin, thenplugin install posthog@posthog. Onnot found in, refresh the catalog and retry; then fall back to the bare name, so the path is never worse than before.execFile. These calls clone git repositories and take seconds.execSyncblocked the event loop, which froze the TUI spinner on its first frame (1 painted frame vs 50 on an async path). ThePluginCapablecontract is unchanged — the methods already returned promises.findClaudeBinarystays synchronous.--jsonlistings and match plugin ids exactly. A plugin namedposthog-somethingno longer reports ours as installed, and a copy installed from any marketplace does. Falls back to the old substring scan when--jsonis unsupported.posthog@*id. Claude Code permitsposthog@posthogandposthog@claude-plugins-officialat the same time, and a bareplugin uninstall posthogremoves only one — somcp removeleft a copy behind.Installing MCP server...while the work runs.enabled, not justid.plugin list --jsonreturns it, and a plugin switched off in/pluginstays listed. Claude Code takes its MCP server from the plugin, so a disabled one serves nothing — and reporting it installed left the user with no server and a screen saying they were done. The install decision now asks whether anything is actually serving; the CLI re-enables on install, so falling through is the repair.isPluginInstalledkeeps meaning present, becauseindex.tsasks it whether there is anything to remove and a disabled plugin still has to be uninstalled. One method cannot answer both questions.repo, not justname.marketplace list --jsonreturns it. Matching on the name alone meant a marketplace merely calledposthogsatisfied the check, so registration was skipped and the install ran against someone else's catalog under our name.installPluginregisters it and removal left it behind, whilecodex.tsclears both halves on the same call — the two clients disagreed about what removal means. Removal now acts when either half is present, guarded on the marketplace being ours by repo, and on it being registered at all (marketplace removeexits non-zero on one that is absent).not found in any configured marketplace— the same string this PR's own acceptance query counts. The cause now travels with the report, so the new root cause stays distinguishable from the old bug. An add the fallback recovers from still reports nothing.PostHog/ai-pluginrather than Anthropic's official marketplace: that one carries this plugin as a pinned sha ofPostHog/ai-plugin.git, so installing through it would gate plugin releases on Anthropic bumping the pin. It also matches what the Codex client already registers.Test plan
pnpm test— 3126 pass.pnpm typecheck,pnpm lintclean.clients/__tests__/claude-code.test.ts: marketplace registered when absent and skipped when present, stale-catalog refresh and retry, bare-name fallback, marketplace-add failure covered by the fallback, exact id matching, multi-id uninstall, and the contract that the path issues noexecSynccall other than binary discovery.$HOME: reproduced the exact production error string, then install → re-install → remove → re-install all clean, with 44 event-loop ticks during a run that previously yielded none.marketplace addis idempotent and exits 0; an SSH clone failure auto-retries over HTTPS.enabledfilter intoisPluginInstalled(which hides a disabled plugin from removal); matching the marketplace on name without repo; dropping the marketplace-removal step; and dropping the marketplace cause from the report.enabledandrepoas the CLI really does, rather than the trimmed shape that let the disabled-plugin defect through.$HOME: disabled ⇒ presenttrue,installPluginruns and the plugin comes back enabled; absent ⇒ presentfalse, installs; enabled ⇒alreadyInstalled, no install call. Also confirmedmarketplace removecascades to the plugins installed from it, and fails on a marketplace that is absent — which is what the two guards are for.mcp removeagainst a sandbox holding both plugin copies removes both.Acceptance
Measure by exception text, not issue id — the reported issue holds only 97 of the 258 occurrences:
Baseline to beat: 54 events / 46 users per 7 days.
Grouping the 29 fingerprints into one (constant exception message, scrubbed properties) and classifying unactionable local-environment failures are follow-up work, not in this PR.
The disabled-plugin defect is not measurable from error tracking — it exits 0 and reports success. Verify by inspection:
Report: https://us.posthog.com/project/2/inbox/reports/01a0c625-d3e6-724e-8809-8e4a14f10d8b
🤖 Generated with Claude Code