← All notes

Bundle Plugins vs Repository Plugins: When to Use Which

Two install paths, different feedback loops. Bundles package config plus code for other people; repository plugins shine when you are iterating inside a monorepo path.

DeepSeek Harness documents more than one way to get code into a profile. If you only ever run dsh plugin add against a github URL, everything feels like “install an extension.” The moment you develop your own plugin—or consume a monorepo that publishes several Cordis packages from one git tree—the bundle versus repository distinction stops being academic.

This note is a selection guide, not a full packaging tutorial. Official plugin docs remain authoritative when command flags move; the decision criteria below change more slowly than CLI spelling.

Side-by-side

BundleRepository / .dsh-plugin
What you distributeConfig rows + code as a unitA source checkout (often with path/ref)
Typical addgithub: / npm: via plugin CLIConsole source + patch / path selector
Feedback loopInstall → often restart webEdit → reload / immediate patch apply
Best forShipping to other peopleLocal experiments, monorepo subpackages
Mental modelPublished artifactWorking tree
Version storyTags / package versionsGit refs and dirty trees

Bundles: the unit you hand to strangers

A bundle is how Harness wants most third-party capability to travel: a package that declares Cordis inserts and the implementation those inserts mount. Official packages under scopes like @deepseek-ai/* and community packages under scopes like @dsh-external/* both fit this world, even though trust assumptions differ wildly. Same packaging shape does not mean same review bar.

Choose a bundle when:

  • Someone else should install you with one CLI line.
  • You care about versioned releases and changelogs.
  • The consumer should be able to patch or replace your row without forking your repo.
  • You want Discover / indexes to point at a stable artifact rather than a moving branch.

As an author, treat the bundle’s config ids as public surface. Renaming an id is a breaking change for anyone who patched you. Prefer additive inserts over surprising replacements of core rows unless you are deliberately providing an alternate implementation of a known service.

Repository plugins: working trees and subpaths

Repository-style installs are for the case where the source of truth is a git tree (or similar) and you may only want one subdirectory. Docs and community writeups commonly show selectors shaped like github:owner/repo#ref&path:packages/foo. The Web console’s plugin surfaces often drive these with a patch file such as cordis.patch.yml, and edits can apply without the full “reinstall the universe” ritual.

Choose repository mode when:

  • You are writing the plugin yourself and changing it every ten minutes.
  • The useful code lives in a monorepo subpath.
  • You want the console and patch file to be the record of what is mounted.
  • You are bisecting a regression across commits and need ref pinning without republishing npm.

The cost is operational: dirty trees, forgotten refs, and “works on my laptop” patches that never become a release. Plan the escape hatch early—when the API settles, wrap the same code as a bundle.

Dependency resolution intuition (just enough)

Cordis composition in Harness is closer to “build a flat closure of plugin rows” than to “npm did it, so we are done.” In practice that means:

  • Your plugin’s inject list is a runtime requirement, not merely a package.json courtesy.
  • Two plugins can both be installed and still fail if the service they expect never appears on ctx.
  • Dumping config is the debugging tool of first resort; guessing boot order is not.
  • A package that installs cleanly can still be inert if its insert targets the wrong profile layer.

When something mysteriously no-ops, ask three questions in order: is the row present, is the service name correct, and did the profile you edited match the process you are looking at? Only then dig into TypeScript build artifacts or HMR.

The uninstall trap

dsh plugin remove is necessary and often insufficient. Removals may leave patch lines behind, or leave you staring at a UI that still believes a row exists. Habit that saves weekends:

  1. Remove with the CLI against the correct profile.
  2. Run dsh --profile web --dump-config (or your profile) and confirm absence.
  3. Open the home / profile cordis.patch.yml and delete stale inserts by hand if they remain.
  4. Restart the long-lived web process.
  5. Spot-check that the feature is actually gone, not merely hidden.
Rule of thumb CLI remove changes intent; dump-config shows truth; the patch file is the ledger when those disagree.

A practical migration path

Teams that start in repository mode often stall when a teammate asks “how do I install your thing?” The migration does not require a rewrite. Freeze a git tag that matches the working tree you trust, publish or point a bundle package at that tree, and keep the same Cordis ids so existing patches still apply. Tell early adopters the repository selector is for contributors; everyone else should take the bundle line from the README.

During the overlap period, run both installs in separate playground homes and diff --dump-config output. Matching ids and services matter more than matching file paths. If the bundle inserts a different id than your repository experiment, you have created two products by accident—rename before you advertise either one.

Selection guide

Productized distribution → bundle. Tag releases, document permissions, keep ids stable, write a one-page “what this can access” note.

Local trial or monorepo subpath → repository. Optimize for iteration, keep the patch file in git, then package a bundle once the shape settles.

Many healthy plugins do both over their lifetime: repository while the API is mushy, bundle when you are ready for other people to depend on you. That progression matches how Harness itself layers profiles—composition first, distribution format second.

Next: security posture for everyday users, including what public 2026 audit threads actually claimed about the plugin axis versus the agent behavior axis.