---
title: Plugin Marketplace
---

# Plugin Marketplace

> Building your OWN plugin from a local source folder (no packaging/marketplace
> steps) is **[Plugin Developer Mode](plugin-dev-mode.md)** — Settings → Plugins.

## What it is

A built-in catalog inside Omniscio that lets you browse, install, update, rate, and uninstall Omniscio plugins — without copying files, editing JSON, or restarting the app. Plugins are first-class extensions of Omniscio: they can add virtual sidebar projects (like RepoGuard), drop new menu entries, expose webview-based UIs, and call a sandboxed bridge into Omniscio's IPC.

The marketplace lives in two places: a **standalone Marketplace view** (Store icon, accessible as a virtual project in the sidebar, with its own category sidebar beside a search + sort toolbar) for browsing, and the legacy **Settings → Plugins** card for plugins you already have installed — which now carries its own **search box, A–Z/Z–A sort, and status (enabled/disabled) + source (built-in/marketplace/dev) filters** over the installed list, shown once you have enough plugins to warrant the bar (the filter/sort logic is the pure, unit-tested [plugin-list-filter.ts](/src/renderer/src/features/settings/plugin-list-filter.ts), and the bar reuses the same `SearchInput` + `Select` primitives the Marketplace toolbar does). The standalone view is where ratings, plugin-detail pages, and the developer dashboard live.

The marketplace solves four concrete problems: (1) you don't have to know which `.json` manifests Omniscio expects, (2) plugin files are checksum-verified so a tampered registry can't silently swap a malicious bundle into your install, (3) updates look after themselves — an update that asks for nothing new is applied in the background, and one that wants new access is a single click — Omniscio compares your installed version against the registry and shows an amber **Update** chip next to anything stale, and (4) ratings + reviews let you see what other users think before installing.

### Availability (ON by default)

The Marketplace is **on by default** since 2026-05-28. The sidebar row and Settings → Plugins browser render for every user. To hide it, toggle **Settings → Workflow → Features → Enable Marketplace** (`marketplaceEnabled`, default `true`).

A one-time reset on **2026-05-26** ([force-off-features-migration.ts](/src/main/services/force-off-features-migration.ts)) originally flipped this flag — along with KMS, Marketplace Review, and Tasks Outliner — OFF for **every** install, including anyone who had been using the pre-flag Marketplace. A follow-up one-shot migration on **2026-05-28** re-enabled marketplace for existing users whose flag had been set to `false` by that reset, and dropped marketplace from the force-off list (KMS / Marketplace Review / Tasks Outliner are still reset). After that one-time fix, your toggle choice is authoritative.

### What ships with Omniscio

Eight plugins are **built in** — they are already on disk when you install Omniscio, so there is nothing to download and they show in **Settings → Plugins** with their source as *built-in*. Only **Nighty Tidy** is switched on for you; the other seven are one toggle away in the same list.

| Plugin | What it does |
| --- | --- |
| **AutoHotkey** (`autohotkey`) | A no-code manager for AutoHotkey hotstrings, hotkeys and snippets — author entries in the app and it generates the `.ahk` file for you. See [AutoHotkey integration](autohotkey-integration.md). |
| **Books** (`books`) | Import and read the EPUB and PDF books you own. See [Books](books.md). |
| **Cashbox** (`cashbox`) | Money in, money out and profit for each of your side projects, captured in plain words; the numbers stay on this computer. See [Cashbox](cashbox.md). |
| **Daily Spend Report** (`daily-spend-report`) | Posts a daily inbox card summarising your AI coding value and real out-of-pocket spend from your live ledger. See [Daily Spend Report](daily-spend-report.md). |
| **Nighty Tidy** (`nightytidy2`) | A standalone code-maintenance audit panel — its own audit list, run history and manual **Start Run**. Ships **enabled**. See [Nighty Tidy 2](nighty-tidy-2.md). |
| **Pattern Oracle** (`pattern-oracle`) | Analyses PRs and sessions across your projects for recurring patterns, errors and optimisation opportunities, and delivers weekly intelligence. |
| **Virtual Pets** (`virtual-pets`) | A pixel-art companion that roams your Omniscio window, reacts to your sessions, and keeps you company. See [Virtual Pets](virtual-pets.md). |
| **Whiteboard** (`whiteboard`) | A freeform infinite canvas for sketching and diagramming, with a board library and an AI agent that can read and draw on a board. See [Whiteboard](whiteboard.md). |

**Foundry** (`prdstack`) is a separate case: it is not bundled, so it arrives through the Marketplace, but Omniscio turns it on for you on first run alongside Nighty Tidy. Everything else in the Marketplace — including the plugins listed in [Plugin CLI Discovery](plugin-cli-discovery.md) — is optional and you install it yourself.

## Where to find it

The Marketplace is a **virtual project in the sidebar** — look for the entry carrying the
**Store icon**, usually labelled **Marketplace**. Opening it gives you the catalogue: a
category sidebar down the left, and a search + sort toolbar above the plugin grid.

What you already have installed is also listed in **Settings → Plugins**, which carries its
own search box, A–Z / Z–A sort, and enabled/disabled + source filters. That page is also
where the per-plugin switches live: the auto-update check, unverified plugin backends, the
session-history access panel, and the one-click re-approval a plugin may need.

Two further views hang off the same feature — the **Plugin Developer** view, a separate
virtual project where a plugin author tracks their own submissions, and **Marketplace Review**,
the admin queue that triages them.

## How it behaves

### Browsing

1. **Open the Marketplace.** Click the **Marketplace** entry in the sidebar (Store icon). It opens as two panes: a left **filter sidebar** (the category list, plus a footer summarizing what you have installed and whether it's current) and the **panel** holding the search box, the sort dropdown, and the plugin grid.
2. **Filter and sort.** Seven categories run down the **sidebar**: **All**, **Planning**, **Development**, **Testing**, **DevOps**, **Productivity**, **Other**. Four sort options live in the **panel** toolbar's dropdown: **Most Popular** (download count), **Highest Rated** (average stars), **Newest** (most recent publish), and **A–Z** (alphabetical).
3. **Search.** The search box sits in the **panel** toolbar beside sort, and matches against plugin name, description, author, and the developer-declared tags. Empty search shows everything in the current category.

> **One control, one home.** These three used to be drawn TWICE — once in the sidebar and
> once in the Browse toolbar — bound to the same store state, so they mirrored each other
> live. Each now has a single home, chosen by what it acts on: **search and sort sit next
> to the grid they reorder**; **categories sit in the sidebar**, where seven items stay
> visible as a column instead of scrolling off the right edge of a toolbar with no
> affordance saying so. The same layout serves desktop and mobile — there is deliberately
> no form-factor variant.
>
> Two things about that split are load-bearing:
>
> - **The sidebar's category labels resolve through `marketplaceView.category*`.** They were
>   raw English literals while the (now-removed) panel pills were translated, so a Spanish
>   user saw an English list mirroring a Spanish one. Now that this is the app's ONLY
>   category filter, an untranslated label would be the only one 17 languages ever see —
>   and no guard catches it, because the hardcoded-string detector cannot see literals
>   inside a const-array of objects.
> - **The panel's search box is the shared `SearchInput`, not a hand-rolled `<input>`.** The
>   box this file used to carry was 14px text with no minimum height (a checked-in lint
>   exemption). `INPUT_CLASS` carries `text-base md:text-sm` + `min-h-tap` precisely because
>   iOS Safari auto-zooms on a sub-16px input and `index.html` sets no `maximum-scale` — so
>   as the app's only plugin search, the hand-rolled one would zoom an iPhone and never zoom
>   back. A test asserts the rendered classes so a future hand-roll cannot quietly return.
>
> On mobile the two panes are separate SCREENS, so `handleSelectCategory` navigates to the
> results screen **unconditionally — including when you re-tap the category you are already
> on**. That is not a redundancy: it is the only route from the filter screen to the
> results. An early-return-if-unchanged would strand the user on a dead list, so a named
> test locks it.

4. **Featured row.** In the default browse state (no search, category **All**) a **Featured** section headlines the view with a curated shortlist of up to three plugins. There is no editorial `featured` flag on the registry — the picks are derived client-side from the two public signals every plugin carries (rating quality and download popularity) via a Bayesian-weighted `curatedScore`, so a single 5-star review can't outrank a broadly-liked, widely-installed plugin. Only plugins with some signal (a rating or a download) are eligible, so a brand-new/empty registry simply hides the row. The moment you search or pick a category the Featured row steps aside, and its picks are held out of the main **All plugins** list below so nothing is listed twice.
5. **Read the cards.** Each plugin card shows the icon, name, one-line description, average star rating, download count, version, author, and category. An amber **Update** chip appears if you have it installed and the registry has a newer version. The download count goes up with every package download (a first install, a reinstall and an applied update all count), so it measures downloads rather than people. Test and throwaway copies of Omniscio do not download Foundry automatically, so they do not inflate its count ([details](foundry-marketplace-only-autoupdate.md)).
   - **Trust signals** (A3) let you judge a plugin before opening it. A blue **Official** badge next to the name marks a plugin built and maintained by the Omniscio team (a **Community** badge marks every other, third-party plugin). Both are derived from a fixed first-party id allowlist in [src/shared/marketplace-trust-signals.ts](/src/shared/marketplace-trust-signals.ts) — never a registry-supplied flag, so a third party can't claim "Official". Below the description a row of **permission chips** shows exactly what the plugin requests: elevated-access capabilities (network, process, filesystem, cloud, or your identity) are amber `warning` chips with a shield icon; benign ones (storage, sessions, notifications…) are neutral. Past four chips the rest collapse into a **+N more** chip, and elevated-risk chips are always shown first so a benign one can't push a risky one under the fold.
6. **Open the detail page.** Click a card's name, icon, description or star rating to open the plugin's full page; the name is a real button, so Tab then Enter opens it too. The card's Install / Update / Uninstall / on-off / Settings controls keep their own jobs and never open the page. The page takes over the Marketplace panel (loaded on demand through `lazyNamedView`, with a skeleton while its code loads) and shows the full description, a **clickable author** that opens the developer's GitHub profile (`openExternalUrl`, plain text when no GitHub handle), a **metadata grid** (category, license, package size, minimum-Omniscio version, last-updated — each field self-omits when absent, and the whole card is hidden if the registry record carries none of them), permissions, version history, install/uninstall button, and the ratings panel (which leads with the average score + review count). Opening it fetches: `selectPlugin(id)` loads the `MarketplacePluginDetail` record (`MARKETPLACE_GET_PLUGIN_DETAIL`) and the first page of reviews (`MARKETPLACE_GET_RATINGS`). **Back**, **Esc** (ignored while typing or while a dialog is open) or the **Browse** breadcrumb returns to the grid and puts keyboard focus back on the plugin's name ([use-plugin-page-navigation.ts](/src/renderer/src/features/marketplace/use-plugin-page-navigation.ts)); a failed load keeps a **Back** button beside **Retry**. At phone width the install/uninstall button wraps under the description.
   - **The page once had no way in for four months.** A May 2026 change dropped the view's only mount of the page while the page itself kept compiling and passing its own tests. The guard [renderer-screens-have-a-production-importer.test.ts](/tests/unit/lint/renderer-screens-have-a-production-importer.test.ts) now fails when any renderer screen loses its last production importer.

### Installing & uninstalling

7. **Install.** From either the card or the detail view, click the blue **Install** button. A consent dialog (`PluginConsentDialog`) appears summarizing the plugin's declared permissions; click **Install** to confirm. The button swaps to a spinner; on success a toast confirms (`Installed <plugin name>`). The plugin's files land in `<userData>/plugins/<plugin-id>/`, the sidebar surfaces any new virtual projects the plugin declared, and any IPC bridges come online. The plugin is also added to `enabledPlugins` in settings.
8. **Uninstall.** On the detail view, click the **Uninstall** button (red, with trash icon). Same spinner/toast pattern; the plugin's directory is deleted recursively and the virtual project is soft-deleted from the sidebar. **Uninstalling also deletes everything the plugin saved** — its records (`plugin_<id>_*` tables), its settings (`plugin_kv`) and its stored passwords (`plugin_secrets`), all in one transaction. Reinstalling brings the plugin back but **not** the data, and the confirm dialog says so before you commit. This is the only path that deletes it: merely _disabling_ a plugin keeps everything (disable is reversible), and so does the automatic check that removes a plugin pulled from the registry — a plugin revoked that way keeps its rows until you uninstall it yourself. A few plugins are exempt outright, either because their data has no second home or because it was migrated out of an Omniscio table and can't be restored by reinstalling; see [plugin-data-purge-contract.md](/.claude/memory/contracts/plugin-data-purge-contract.md).
9. **Update.** In-place reinstall — click **Install** again on a row showing the **Update** chip and Omniscio atomically swaps in the newer files. Local plugin data is untouched; only the bundle files are replaced.

### Auto-update (applies the safe ones, asks about the rest)

Omniscio **keeps your installed plugins current by itself**, and stops only where it would otherwise hand a plugin access you never agreed to.

- An update that **requests nothing new** is installed automatically, in the background, with no click. This is the common case, and it is why you rarely have to think about plugin versions at all.
- An update that **wants access the running version did not have** — a new permission, or a background service where there was none — is **never** applied on its own. It waits, with the permission list shown, until you say yes.

Both decisions are made in the app's main process, against the plugin's actual downloaded package rather than the marketplace listing, so a listing that under-reports what a version asks for cannot slip past. A refusal leaves your installed version exactly as it was. Four more cases wait for you rather than update: a **disabled** plugin (updating one would switch it back on), one whose stored approval is already behind its installed version, one you are running as a **developer build**, and anything the app cannot read.

When an update does wait for you, you see three non-intrusive cues:

- A **count badge** on the Browse tab and a dismissible **"N plugin updates available · Review all"** banner at the top of the Browse view.
- An **inbox card** ("N plugin updates available") with a **Review updates** button, which opens the Updates Review modal straight from the inbox. It lists what is waiting by name and version, so the card answers "what changed?" on its own without opening anything. It dedupes on the exact set, so the launch check, the 6-hour interval check and the window-focus re-check all collapse onto one row — and a _newly_ published update still raises a fresh card after you dismissed the last one.
- The **Updates Review modal** (opened by the badge/banner/card). It lists each waiting plugin with its version bump (`v1.0.0 → v2.0.0`), a short changelog snippet, and — critically — the **permissions the new version newly requests** (a "Requests new:" row). You apply per-row with **Update**, or all at once with **Update all**.

> **A background update is silent on purpose; a broken one is not.** The app updates *itself* the same way — quietly, with no popup — and this subsystem already had a plugin-update toast removed three times, so nothing here raises one. The single exception is a half-landed update: if a plugin's files are replaced but its background service fails to restart, you get the same warning the manual path gives, because an enabled plugin with a dead service is otherwise silent.

> **Why a card and not a toast for the ones that wait.** The check runs three ways you never asked for (launch, every 6 hours, every window focus), so a toast popped over whatever you were doing — and then auto-dismissed, routinely taking its own **Review** button with it before it could be clicked. The card is raised over `MARKETPLACE_NOTIFY_UPDATES` and keyed by [plugin-updates-alert.ts](/src/shared/alert-features/plugin-updates-alert.ts). The toast was removed and came back three times before this stuck, so it is now banned repo-wide rather than merely removed — re-adding it fails [plugin-updates-never-a-toast.test.ts](/tests/unit/lint/plugin-updates-never-a-toast.test.ts), which also fails if the card is dropped without a replacement.

The check runs on launch (deferred, after first paint), on a 6-hour interval, and on window focus. It's **on by default** and reuses the existing registry read (`MARKETPLACE_FETCH_REGISTRY`) — no new network surface. Turn it off at **Settings → Plugins → Check for plugin updates** (`pluginAutoCheckUpdates`, default `true`); with it off, nothing is checked and nothing is updated in the background. It's also gated on `marketplaceEnabled`, so hiding the marketplace disables it too.

**Check on demand.** Two buttons run the check immediately, for anyone who'd rather not wait for the automatic pass:

- **Marketplace sidebar**, in its own footer below the category list (`MarketplaceSidebarFooter.tsx`). The footer renders one of four states:
  - **Loading…** — before the first registry read has completed.
  - **Couldn't reach the marketplace** — the first read failed outright.
  - **No plugins installed yet**, with a **Browse plugins** link that clears the category filter back to **All** (and, on mobile, also drills straight into the results grid) — there's nothing to check yet, so the button below is omitted entirely.
  - Otherwise, a three-tier hierarchy, top to bottom: your **installed count** (e.g. `5 installed`) headlines the block; a **status line** underneath reads `Up to date`, `N updates`, or `check failed`, with a `checked {when}` freshness stamp that ages every 60 seconds (the stamp is omitted on failure — the line turning red already says so); the **Check for updates** button sits below that. It's shown whenever at least one plugin is installed, including when everything is already current — the update badge/banner/card only appear once updates are _already_ known, so with nothing to report the Marketplace previously offered no way to ask at all.

  The installed count **replaced** an earlier category-derived plugin total that silently tracked whichever category the user had last clicked in the list above it (never actually a total) and, at public scale, told the user nothing they could act on — "2,847 plugins" says nothing about _you_.

- **Settings → Plugins**, a **Check now** button beside the auto-check toggle.

They differ in how they answer, and deliberately so. The **sidebar** button answers _in place_, on its own status line and freshness stamp — two separate lines, not one — because that footer sits inside one bordered block with the button and persists, so question and answer share one region. The **Settings** button has no such line next to it (`reportUpToDate`), so it is never a silent click: "All plugins are up to date" and a failed check still toast, and a check that _finds_ updates opens the Updates Review modal right there rather than pointing you at an inbox card you did not know you were getting. Either way the pending set also lands in the inbox, so closing the modal loses nothing.

> **A press must always be visible, even when nothing changed.** The sidebar button used to feel broken: pressing it while everything was already current changed **nothing on screen** — the status line already showed the verdict, the forced re-fetch commonly answers in ~200ms (too fast for a spinner to read as anything but a flash), and the sidebar button deliberately never toasts (see above). A user genuinely could not tell a working button from a dead one. The fix, entirely in `MarketplaceSidebarFooter.tsx`:
>
> - The button's label swaps to **Checking…** and HOLDS that state for a minimum window (`MIN_PENDING_MS`, 700ms) even when the reply is instant, so the press is never a same-tick flash.
> - The freshness stamp resets to **"checked just now"** unconditionally, whether or not the verdict changed. This is the load-bearing part: "Up to date" is already on screen before the press and stays there after it, so the stamp is the _only_ thing a successful press can ever visibly change. Skip the reset and a click that finds nothing new goes back to being silent — by design this time, instead of by accident.
> - On success only, the status line takes a brief accent pulse (dropped under `prefers-reduced-motion`) — never on failure, since the line turning red already says the press did something.
> - A screen-reader-only live region announces the outcome in the same words the visible line shows, and **only a manual press writes to it**. The automatic checks (launch, the 6-hour interval, window focus) still keep the stamp truthful, but must never claim the acknowledgment a manual press earns — an automatic refresh writing to that region would interrupt a screen-reader user roughly once a minute forever, and again on every alt-tab back into the app.

Both buttons spin while working and ignore a second click until the first settles (`AsyncButton`). Neither installs anything itself: a check asks main for the pass that applies the updates requesting nothing new (see above), and anything wanting new access still waits for your click.

**A manual check is a REAL check.** `fetchRegistry()` is TTL-cached for 5 minutes (`CACHE_TTL_MS`) and opening the Marketplace warms that cache on mount, so an unforced click would have answered straight from cache — reporting "up to date" without a single network call. Both manual buttons therefore pass `force`, which makes the handler drop the cache (`invalidateRegistryCache()`) before fetching. The **automatic** check deliberately does _not_ force — the TTL still shields the remote registry, and a test locks that asymmetry. `force` is an optional field on the existing `MARKETPLACE_FETCH_REGISTRY` read: no new IPC channel, and the shared marketplace rate limit still applies.

**`force` means "the user asked for this", and carries three things at once** — they're one concept, so they ride one flag ([marketplace-store.ts](/src/renderer/src/stores/marketplace-store.ts)):

1. **Drop the main-side TTL cache** — see above.
2. **Bypass the renderer's in-flight dedup** — a click moments after mount would otherwise piggyback the very cached load it meant to skip.
3. **Never touch `isLoading`** — that flag swaps the whole plugin grid into skeleton boxes ([BrowseTab.tsx](/src/renderer/src/features/marketplace/BrowseTab.tsx)), so a manual check used to blank the catalog the user was reading, for no visible reason. A forced load skips the flag entirely; the button's own spinner is the acknowledgment.

A forced load also **does not eagerly clear `registryError`**. Clearing it at load start made the status line read "up to date" — no error plus a stale count — while the check was still running, a confident wrong answer moments before it snapped to "check failed". The line now holds its last honest value until a real result lands.

**Concurrent loads are ordered, not last-write-wins.** Because a forced load bypasses the dedup, it runs alongside the mount's cached load and both write `plugins` on settle. Without ordering, the older cached response could land second and silently overwrite the fresh registry the user explicitly asked for — and the sidebar would then report against data it had just discarded. A monotonic request id gates the write, so only a response at least as new as the newest one already applied may commit.

**Permission escalation is never silent.** When you apply an update whose new version requests a permission the installed one didn't have, the consent dialog (`PluginConsentDialog`) highlights each new permission with a **New** badge and leads with "This update requests new permissions." The `diffPermissions` helper ([src/shared/plugin-permissions.ts](/src/shared/plugin-permissions.ts)) computes installed-vs-incoming and drives both the modal row and the consent dialog.

### Version compatibility (a plugin can be refused)

A plugin declares two version requirements, and Omniscio checks both when it loads one. If either fails, the plugin is marked incompatible: it stays visible in Settings but is withheld from the sidebar and the toolbar, and Omniscio records a plain-language reason for why.

- **`minAppVersion`** — the plugin needs a newer Omniscio than you are running ("Needs Omniscio 0.2.0 or newer (you have 0.1.86)."). Fix by updating the app.
- **`sdkVersion`** — the plugin was built against a version of the plugin SDK this Omniscio build does not implement ("Built for plugin SDK ^3.0.0, but this version of Omniscio provides 2.0.0."). Fix by updating the app, or by the author publishing a build for the SDK you have.

This build implements SDK **2.0.0**, matching the published `@agent-mc/plugin-sdk` on npm. Keeping those two numbers equal is the whole job: the check refuses anything _above_ what the app implements, so if the app lags the published SDK, an author who installs the current SDK and declares the version they installed gets a plugin this app will not load — and `sdkVersion` is a required manifest field, so they cannot opt out of the mismatch. That is exactly what happened between 2026-08-11 and 2026-08-28, when the SDK went to 2.0.0 and the app still said 1.2.0 (a version npm never had).

The `sdkVersion` half is new as of **2026-07-27**. Before that the field was read only to tell an old-style plugin from a worker-isolated one, and was never checked for compatibility — so a plugin built against a future, incompatible SDK loaded anyway and failed somewhere deep in a bridge call with nothing pointing at the real cause. The check is what makes a future SDK major version safe to ship.

The check is deliberately forgiving, because wrongly refusing a working plugin is worse than a clear runtime error. A plugin with no `sdkVersion` is always accepted. A bare version like `1.0.7` (which every plugin published so far declares) is read as a **minimum**, not an exact match, so those keep working as the SDK moves forward — that is why the move to a 2.0.0 host refused nothing already in the marketplace. Only a real range like `^3.0.0` can actually refuse, and an unparseable value is ignored rather than treated as a failure.

### Workspace-only plugins

A plugin can be limited to one workspace. Its manifest names the workspace (`plugin.requiresOrganization`), and Omniscio then shows it in **Settings → Plugins** and lets you turn it on only while that workspace is your **active** one. Anyone else never sees it, and turning it on from the CLI (`POST /plugins/:id/enable`) is refused with a plain "only available to members of its workspace" message. Switch to the right workspace and it appears.

This hides the plugin; it does not protect data. The plugin's code ships in every install, so a workspace-only plugin must keep its data behind its own server-side lock — the first one, the [JLS SOP Assistant](jls-sop-assistant.md), keeps its library behind database rules that refuse anyone outside the workspace.

### Third-party plugin backends (security gate)

Some plugins ship a **backend** that runs with full system access (file system, network, child processes). Omniscio cannot verify a third-party (marketplace) plugin's backend, so by default it is refused at activation. The plugin looks like it enables, but its backend never starts and the toggle rolls back. To run one, turn on **Settings → Plugins → Allow unverified plugin backends** (`allowUnverifiedPluginBackends`, default `false`). Only do this if you trust the plugin's author. Built-in plugins ship with Omniscio and are always allowed, so this toggle never affects them.

**An update to a running plugin takes effect immediately — its background half is restarted as part of the update.** A plugin's background half is a separate program that loaded its code when it started, so swapping the files underneath it changes nothing about what it is executing. Applying the update therefore stops that program and starts a fresh one from the new files, and the update is not finished until it has. Before this, an update replaced the files but left the old program running: the plugin kept behaving as the previous version for the rest of the session while the version number, the files on disk and their checksums all reported the new one — a mismatch nothing surfaced. If the restart itself fails you are told so ("… its background service didn't restart"), the plugin stays switched on, and the next launch retries it. A floating overlay window and a popped-out panel are refreshed by the same update, so they cannot keep showing the previous version's screen either.

**A background plugin stays approved across its updates — and tells you exactly which one if it doesn't.** When you apply an update to a plugin whose background half is running, Omniscio records your permission for the new version (whichever way you approved it, you were shown the newly-requested permissions first — see the next section), so the restart above does not stop to re-ask, and the next launch cannot strand the plugin on a stale permission record either. In the rare case a background plugin still needs re-approval at startup — for example, an older plugin whose stored permission is out of date — Omniscio raises a one-time inbox notice that **names the affected plugin** ("**GitHub** needs your permission again"; for two, "**GitHub** and **Calendar** need…"; for more, "**GitHub** and 2 others…") and deep-links to **Settings → Plugins**. There the plugin's own card carries a persistent amber **"Needs your permission"** callout and a one-click **Re-approve** button that re-grants and restarts it in a single step (no toggle off-then-on) — and opening the page scrolls straight to it. It stays one notice no matter how many plugins are affected, and never silently leaves a plugin's background half dead and looping.

That same **"Needs your permission" callout + Re-approve** now appears everywhere you might meet the dead plugin, not just Settings: on its **Marketplace card** and its **Marketplace detail page** (so browsing to a misbehaving plugin offers Re-approve instead of only Uninstall). And the moment you **open** a plugin that needs re-approval — when its panel would otherwise just sit there loading, because the UI opens but its background half is dead — a toast fires that **names the plugin** ("**GitHub** needs your permission again") with a **Re-approve** button that jumps straight to its callout in Settings. The toast shows once per open, never stacks, and stays silent for a healthy plugin and on mobile (where a plugin backend can't run at all). All four surfaces (Settings, the Marketplace card + detail, and the toast) reuse ONE shared callout component and the existing translated strings — so they can never drift, and the recovery is reachable from wherever you are.

### When an agent asks to install a plugin (the approval card)

An agent can request a plugin install over the CLI control server (`POST /plugins/install`). It cannot install anything on its own — the request becomes an **approval card in your inbox**, and nothing is downloaded until you approve it.

**The card tells you what approving actually grants.** It names the plugin, the exact version being installed, and the permissions that version is asking for. If you already have the plugin, the card reads as an update — `Update plugin "Foo" from v1.4.0 to v2.1.0` — and lists only the permissions the new version wants that you have **not** already granted, the same "requests new" comparison the in-app Updates modal shows. An update that asks for nothing new says exactly that, so a card with no permission line never leaves you guessing whether it was checked. Every requested permission is listed; the card never abbreviates the list, because the order they appear in is chosen by the plugin's author and a shortened list could hide the one that matters.

This matters because asking to install a plugin you **already have** is how an update gets applied. Approving the card is what grants the new version's permissions, so the card has to say what they are.

**Clicking Approve shows you what the plugin will be able to do, the same way the Marketplace does.** Approving an agent's install request IS the consent — nothing prompts you a second time afterwards — so the confirm that opens lists each capability in plain words ("Storage — read and write plugin data, files, and database records"), flags elevated ones, and calls out separately that the plugin runs its own background program with access the permission list cannot limit. It is an amber caution, not a red destructive warning, because installing a plugin deletes nothing; it says so outright, and Enter defaults to Cancel so a stray keypress cannot grant anything. Turning a plugin on or off reads the same way. Uninstalling keeps the red warning, because it really does delete the plugin's files and everything it saved. If the capability list genuinely cannot be loaded, the confirm shows the card's own description instead of an empty list, so a failed lookup never reads as "this plugin asks for nothing".

**If Omniscio can't describe the install, it won't ask you to approve it.** A plugin id the marketplace doesn't carry, a marketplace it can't reach, or a listing it can't make sense of all refuse the request outright rather than raising a vague card. You will never be asked to approve an install described only as "runs third-party plugin code".

**What you approve is what installs.** The version named on the card is locked in. If the marketplace publishes a newer version while the card is sitting in your inbox, approving it does **not** quietly install that newer one — the install is refused and you are told to request it again, so you can see what the new version is asking for. A repair or reinstall of the same version is unaffected.

Installing from the Marketplace yourself is unchanged: you see the permissions on the detail page and in the consent dialog before you click, so there is no gap for the version to move in.

## For agents

The rest of the Marketplace — what a plugin may read from your history and boards, what it can
watch you doing, ratings and review submission, the error taxonomy, the developer and publisher
surfaces, package signing, and the registry, install and IPC internals — is in
[Plugin Marketplace (part 2)](plugin-marketplace-part-2.md).

## Related

- [plugin-cli-discovery.md](plugin-cli-discovery.md) — discovering and driving installed plugins from any session via CLI
- [repoguard.md](repoguard.md) — first plugin shipped through the marketplace
- [prdstack.md](prdstack.md) — second plugin shipped through the marketplace
