can1357/oh-my-pi · error · Error
Plugin "${pluginId}" is already installed. Use force option
Error message
Plugin "${pluginId}" is already installed. Use force option to reinstall. What it means
installPlugin guards against silently overwriting an existing installation: it looks the plugin up in the installed-plugins registry, and if an entry already exists and force was not passed, it refuses to proceed. Passing force: true opts into reinstallation.
Source
Thrown at packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts:270
const mktEntry = getMarketplaceEntry(mktReg, marketplace);
if (!mktEntry) {
throw new Error(`Marketplace "${marketplace}" not found`);
}
// 2. Find plugin in catalog
const catalog = await this.#readCatalog(mktEntry);
const pluginEntry = catalog.plugins.find(p => p.name === name);
if (!pluginEntry) {
throw new Error(`Plugin "${name}" not found in marketplace "${marketplace}"`);
}
const pluginId = buildPluginId(name, marketplace);
// 3. Check if already installed
const instReg = await readInstalledPluginsRegistry(registryPath);
const existing = getInstalledPlugin(instReg, pluginId);
if (existing && existing.length > 0 && !force) {
throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
}
// 4. Resolve source path.
// marketplaceClonePath is the marketplace root — the directory containing .claude-plugin/
// catalogPath is <marketplacesCacheDir>/<name>/marketplace.json, so the root is two levels up.
// For local sources the content was fetched from a local path; the stored catalog is a copy
// under marketplacesCacheDir. We need the original source root for resolving relative paths.
// Use: path.dirname(catalogPath) is <cacheDir>/<name>/, and that IS the stored copy root,
// so `path.resolve(mktEntry.catalogPath, "../..")` = parent of <name>/ inside cacheDir
// which is wrong for local sources. Instead, derive from the stored catalog directory:
// stored at: <marketplacesCacheDir>/<catalogName>/marketplace.json
// The marketplace root for local sources should be the actual local path, but we only have
// sourceUri. For local sources, use path.resolve of sourceUri; for others use the cache dir.
const marketplaceClonePath = this.#resolveMarketplaceRoot(mktEntry);
// URL-sourced marketplaces only cache marketplace.json, not the full plugin tree.
// Relative string sources ("./plugins/foo") cannot be resolved against the cache dir.
if (mktEntry.sourceType === "url" && typeof pluginEntry.source === "string") {View on GitHub (pinned to 9690622007)
Solutions
- Pass the force option ({ force: true }) to reinstall over the existing copy
- If the plugin is fine as-is, skip the install — it is already present
- Uninstall the plugin first (uninstallPlugin) then reinstall cleanly
- Inspect the installed-plugins registry to confirm the existing entry and remove stale leftovers if needed
Example fix
// before
await manager.installPlugin("formatter", "community");
// after
await manager.installPlugin("formatter", "community", { force: true }); Defensive patterns
Strategy: try-catch
Validate before calling
const instReg = await readInstalledPluginsRegistry(registryPath);
const already = getInstalledPlugin(instReg, buildPluginId(name, marketplace));
if (already && already.length > 0 && !force) {
// skip install or set force
} else {
await manager.installPlugin(name, marketplace, { force });
} Try / catch
try {
await manager.installPlugin(name, marketplace);
} catch (err) {
if (err instanceof Error && err.message.includes("already installed")) {
// desired state already reached; treat as success
} else throw err;
} Prevention
- Make install scripts idempotent: catch "already installed" and treat as success
- Use explicit force: true when you intentionally want to overwrite an install
- Clean up stale registry entries after failed/aborted installs
- Track installed plugin ids in one place to avoid duplicate install attempts
When it happens
Trigger: Calling installPlugin(name, marketplace) without options.force when buildPluginId(name, marketplace) already has entries in the installed plugins registry; also reachable via upgradePlugin if it does not set force.
Common situations: Re-running an install command that already succeeded; a failed/partial earlier install left a stale registry entry; installing the same plugin from two scripts; wanting to overwrite local modifications to an installed plugin.
Related errors
- Edits to ${path} resulted in no changes being made.
- Plugin "${name}" not found in marketplace "${marketplace}"
- Plugin "${name}" uses a relative source path but marketplace
- Plugin "${entry.name}" lspServers path escapes the plugin di
- Plugin "${entry.name}" dapAdapters path escapes the plugin d
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d73551cb69c2f942.
Report an issue: GitHub.