Skip to content

Managed plugins are never re-synced from the server after install — local edits/deletions to plugin content are neither detected nor corrected (enforcement silently defeatable) #4933

Description

@rbob45359

Describe the bug

Once an enterprise-managed plugin (delivered via extraKnownMarketplaces + enabledPlugins in managed-settings.json) is installed, Copilot CLI never re-validates or re-syncs its content from the marketplace/server. The on-disk plugin files are user-writable and unsigned, and no session start, periodic refresh, or re-login re-fetches them from the source. The enabled flag in config.json is policy-locked, but the plugin content (e.g. its hooks.json) is not protected at all.

As a result, a developer can neutralize a managed plugin's hooks — with no elevated privileges and no error surfaced — by editing the hook file, deleting it, or repointing it. Nothing built into Copilot detects the change or restores the server's version.

For teams using managed plugins as a governance / policy-enforcement mechanism, this means the managed-plugin persistence guarantee does not hold: policy-pinned hooks can be silently and permanently disabled locally.

Steps to reproduce

  1. Configure an enterprise-managed plugin containing a hooks.json, via managed-settings.json (extraKnownMarketplaces + enabledPlugins). Confirm it installs and its hooks fire (e.g. a hook that appends to a log on preToolUse).
  2. Confirm it is synced on disk under ~/.copilot/installed-plugins/<marketplace>/<plugin>/ (and mirrored in the marketplace cache under ~/Library/Caches/copilot/marketplaces/<slug>/), and that the log records hook events.
  3. Tamper with the plugin in any of the ways below.
  4. Start a new Copilot CLI session (full relaunch) and run a tool.

Actual behavior

  • Editing a managed plugin's hooks.json is never detected or corrected — the tampered content runs on every subsequent launch, with no error.
  • Relaunching, signing out and back in, and the periodic refresh all leave tampered content in place; none re-fetch from the source.
  • source_sha in config.json does not guard integrity — it is recomputed from whatever is on disk and re-stamped to match the tampered content.
  • Deleting the whole plugin folder does trigger a re-sync, but it restores from the local (user-writable) marketplace cache, so tampered content is re-instated rather than corrected.
  • Deleting only hooks.json (leaving the folder in place) is not re-synced at all — the plugin still shows installed/enabled, but its hooks never run again and are never restored.

This looks related to the "Re-sync managed plugins when their cache is missing or empty" behavior (see #4039): the re-sync guard keys on whether the cache directory exists and is non-empty, rather than on whether the content matches the source — so any tamper that leaves a non-empty directory (an edit, or a single-file delete) is neither detected nor corrected, and the one guard that does fire restores from a local cache rather than re-fetching from the source.

Expected behavior

At session start (and ideally on the documented periodic refresh), Copilot should verify each managed plugin's on-disk content against the version declared by the marketplace/policy — e.g. a manifest or content hash fetched from the source, not the recomputed local one — and re-fetch from the source on any mismatch. This should cover edits and partial deletions, not just a missing/empty folder, and should never treat the local cache as authoritative for a policy-pinned plugin. If restoration is not possible, the tampered/incomplete state should surface as an error rather than silently running tampered content or reporting "no hooks."

Environment

  • Copilot CLI version: 1.0.87
  • OS: macOS
  • Delivery: enterprise managed-settings.json via .github-private (extraKnownMarketplaces + enabledPlugins)
  • License: Copilot Business / Enterprise

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions