SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

dsh plugins: how they work and how to vet them

Installing a dsh plugin runs someone else's code with your agent's permissions. What a plugin can reach, and how to check one before you install it.

What is a dsh plugin, and what can it do?

dsh plugins are Node packages that the DeepSeek Harness loads into its own process. Installing one runs somebody else's code with your agent's permissions, on the machine your agent can already reach. Nothing stands between a loaded plugin and the rest of the harness. So the question to ask before you install one is what that code can touch, and how you keep that small.

dsh (DeepSeek Harness) is DeepSeek AI's open-source agent harness, built on a plugin framework called Cordis. The project's own README says that everything is a plugin. The model adapter is a plugin. The web interface you type into is a plugin. Anything you install from outside the project lands in the same tree, at the same level of trust as the parts that shipped with it. If you have not stood one up yet, start with the DeepSeek Harness on a VPS and come back before you add anything to it.

The extension points a plugin can reach are listed in the repository's AGENTS.md. As of August 2026 they cover:

  • LLM (large language model): the provider your API key pays for
  • Shell: the bash capability, with local and pwsh providers
  • Filesystem: policy-controlled file access
  • Web: search and fetch providers
  • Subprocess: a process-tree provider
  • Workflow: worker threads
  • Subagent: delegation to further agents
  • Settings and credentials: your saved configuration and environment variables

A plugin also registers tools on ctx.tools, and the docs are explicit that a registered tool's schema joins prompt assembly. That second half is the part people miss. A plugin can change what your agent decides to do without its own code doing anything unusual, because the description it contributes becomes text the model reads. That is the same shape of problem as prompt injection against coding agents, with one difference: this text arrives when you install, and it stays until you remove the plugin.

How does dsh find and load plugins?

There is no global plugin directory. A running dsh is a plugin tree composed at boot from ordered layers, and the unit holding your choices is a profile. $DSH_HOME defaults to ~/.dsh, and each profile lives in $DSH_HOME/profiles/<name>. The web and headless profiles create themselves on first use from shipped templates.

A profile directory holds two files that decide everything:

  • package.json, with the out-of-tree plugin dependencies plus a dsh.profile manifest carrying the ordered bundles list
  • cordis.patch.yml, your own patch layer over those bundles
ls ~/.dsh
ls ~/.dsh/profiles/web

Boot applies the layers in this order, and later layers win:

  1. an empty root
  2. the profile's bundles, in the order the manifest lists them
  3. the profile's cordis.patch.yml
  4. $DSH_HOME/cordis.patch.yml
  5. any --patch <path> overlays passed on the command line

Two flags print the result of that composition without starting anything:

dsh --profile web --dump-default-config
dsh --profile web --dump-config

--dump-default-config prints the composed tree on its own. --dump-config adds the profile and home patch layers, so it is the closest thing to an honest inventory of what your next boot will load. Read it before you trust a machine you inherited.

One warning about those patch files. Config is not inert data here, because the format allows !!js tagged values under a plugin's config block. A cordis.patch.yml snippet copied from a forum post is code, so treat it the way you would treat a shell script from the same source.

What does dsh plugin add actually run?

dsh plugin --profile <name> <args> forwards its arguments to pnpm inside that profile's directory, so pnpm has to be on PATH. The verbs are pnpm's verbs:

dsh plugin --profile web add '<package-or-git-spec>'
dsh plugin --profile web remove '<package-name>'
dsh plugin --profile web why '<package-name>'
dsh plugin --profile web update

The security model of installing a dsh plugin is therefore the security model of installing any npm-style dependency, plus one extra step where the result gets loaded into your agent. The package brings its own dependency tree, and every package in that tree ends up in the same process. Everything in how npm supply chain attacks reach a server applies here without modification.

pnpm 10 and later do not run a dependency's build scripts by default, and approval is per package through onlyBuiltDependencies or pnpm approve-builds. Check which pnpm you have:

pnpm --version

That default is worth having, and it is also the most over-read safety feature in the ecosystem. Blocked build scripts stop code from running during the install. They do nothing about the plugin itself, because the whole point of a plugin is that the harness imports it and calls it on the next boot. A plugin does not need a postinstall hook. It was invited in.

What to read before you install a dsh plugin

Download the published tarball and read it. Nothing executes when you unpack an archive.

npm pack '<package-name>@<version>'
tar -tzf '<package-name>-<version>.tgz'
tar -xzf '<package-name>-<version>.tgz'
less package/package.json

Four fields in that package.json tell you most of what you need. Read scripts for preinstall, install and postinstall entries. Read dependencies for names you do not recognise, or names that are one character away from ones you do. Read bin for anything the package wants on your PATH. Read main or exports for the entry file, then open that file and follow it.

Then read the code that will actually load. A plugin that advertises a notification tool has no reason to read ~/.ssh, call a host you have never heard of, or spawn a shell. If the package ships only bundled or minified JavaScript and no matching source exists in a public repository, that is your answer. Prefer plugins whose source you can read, and prefer small ones.

You can also question the registry without installing anything:

pnpm view '<package-name>' dependencies
pnpm view '<package-name>' versions

A package published last week, with one version, no repository field and a name that shadows something popular, is the oldest trick in any registry. Verifying downloads with checksums is the habit next door: know exactly what you fetched before you let it run.

Pin the version, and keep the lockfile

A floating version range means the code inside your agent's process can change on any install or update without you deciding anything. Pin it.

dsh plugin --profile web add --save-exact '<package-name>@<version>'

Flag placement varies between pnpm versions, so check the result instead of trusting the command. Open the profile's package.json afterwards and confirm the dependency reads as a bare version with no ^ or ~ in front of it. That file decides what installs.

Then keep the lockfile, which pins the whole transitive tree rather than only the top-level name:

find ~/.dsh -maxdepth 3 -name 'pnpm-lock.yaml'

Copy it somewhere you back up, together with the profile's package.json. Those two files rebuild the same tree on a new box. Run dsh plugin --profile web update when you have decided to move versions, never as routine tidying, and read the lockfile diff afterwards.

For a plugin installed from git rather than a registry, pin the commit instead of the branch. A spec of the form github:owner/repo#<full commit sha> gives you a fixed tree. A branch name gives you whatever that branch holds the next time pnpm resolves it, which is a decision you have handed to someone else.

The plugin market, and what "curated" is worth

dsh has a marketplace, and it installs as a plugin, which tells you something about the architecture:

dsh plugin --profile web add dshmarket

After a restart it appears under Settings, then Plugin Market. Its README is plain about the limits. Installs are restricted to sources listed in a curated registry and anything else is rejected. Build scripts are blocked by default, and enabling one takes per-package approval. Terminal plugins are flagged before they go into a web profile. The sentence that matters most is that listing is not endorsement, because the plugins are third-party code.

A curated list raises the floor. It does not read the code for you, and it cannot tell you what the next version of a plugin will do after a maintainer account changes hands. Treat a one-click install the way you would treat curl | bash from the same author. One more line from that README is worth repeating: an exported backup can contain credentials out of your profile config, so never attach one to a public issue or a paste site. If you want a starting list rather than a method, dsh plugins worth installing is the companion post to this one.

Run dsh as its own user, not as root

Vetting reduces how often something bad gets in. Least privilege decides what it reaches when it does. On a VPS that second half is cheap to set up.

Give the harness its own unix account with its own home, and never run it as root:

sudo adduser --disabled-password --gecos "" dshrun
sudo -iu dshrun

Inside that session, start the harness so it writes its home under that account:

npx @deepseek-ai/dsh web

The web UI serves on http://127.0.0.1:3080 by default. Leave it there. Anything that reaches that port can drive an agent that has a shell, so publishing 3080 is the same as publishing a root-less remote shell with a friendly interface. Reach it from your laptop over an SSH tunnel instead:

ssh -L 3080:127.0.0.1:3080 you@your-vps

Then confirm nothing is listening on a public address:

ss -lnt | grep 3080

The local address should read 127.0.0.1:3080. If it reads 0.0.0.0:3080, your firewall is the only thing between a stranger and your agent. The reasoning behind running Claude Code safely on a VPS applies to dsh unchanged. Give the agent one workspace directory it is allowed to ruin, and keep anything you cannot rebuild off that machine. Better still, treat the box as a disposable VM for coding agents, because rebuilding a VPS costs an hour while auditing one costs a week.

Where your keys live, and why file permissions only go so far

dsh keeps API keys in $DSH_HOME/.credentials.yaml and environment values in $DSH_HOME/.env, with model settings in $DSH_HOME/settings.yaml and session history under $DSH_HOME/storages. Lock down the sensitive two:

chmod 600 ~/.dsh/.credentials.yaml ~/.dsh/.env
ls -l ~/.dsh

Mode 600 gives the owner read and write and gives everyone else nothing, which is worth knowing in both notations (numeric versus symbolic chmod modes). Be honest about what it buys. File modes protect those files from other accounts on the box. They do nothing against a plugin, because the plugin runs as the user that owns them, inside the process that reads them. That is why keeping secrets out of an AI agent's reach means not putting them on the machine at all. A dsh box should hold the one model key it needs. Your cloud credentials and your signing keys belong somewhere else.

Why a plugin that reads the web changes the threat model

The Web seam gives plugins search and fetch providers. A plugin that pulls a page into your session is pulling text an attacker can write. A model prompt does not separate instructions from data, so a fetched page can carry a line addressed to your agent, and a harness holding the shell capability is one obedient step from running it.

The control already exists in the harness. dsh-base, the first bundle in every profile, ships the sandbox and approval policy. Use it. A session that can fetch untrusted pages should require approval on anything that writes or executes, so a fetched instruction cannot become an action on its own. Gating agent actions behind approvals covers how to think about where that line goes. The relationship runs in both directions too, since your own server is a page that somebody's agent will fetch, which is the case for blocking AI crawlers on your server.

How do I check what a plugin changed?

Snapshot before, install, snapshot after, then read the difference.

dsh --profile web --dump-config > /tmp/dsh-before.txt
dsh plugin --profile web add '<package-name>@<version>'
dsh --profile web --dump-config > /tmp/dsh-after.txt
diff -u /tmp/dsh-before.txt /tmp/dsh-after.txt

The diff shows which plugin entries the install added to the composed tree. A plugin you installed for one small feature that adds several entries you cannot account for is a reason to stop and read the source before you boot it. dsh plugin --profile web why <package-name> answers the other question: which of your direct dependencies pulled a given package in.

Installed packages land under $DSH_HOME/profiles/node_modules, so you can also look at the tree on disk:

ls ~/.dsh/profiles/node_modules

Keep a second profile you never experiment in. When an install breaks the harness, booting dsh --profile <clean-name> tells you in seconds whether the plugin caused it.

How do I remove a dsh plugin?

dsh plugin --profile web remove '<package-name>'
dsh --profile web --dump-config > /tmp/dsh-after-removal.txt
diff -u /tmp/dsh-before.txt /tmp/dsh-after-removal.txt

Removing the dependency does not always remove the configuration. Entries written into the profile's cordis.patch.yml stay where they are, because that file is yours and the harness will not rewrite it for you. Open it and delete any block naming the package you dropped.

less ~/.dsh/profiles/web/cordis.patch.yml

Then face the part no uninstall command can fix. If you removed a plugin because you stopped trusting it, whatever it could read, it has already read. Rotate the DeepSeek API key in the provider console, and rotate anything else that was sitting in $DSH_HOME. Then work out what the unix account it ran as could reach on the rest of your network.

The short version

  • Read the published tarball before the install, starting at scripts and the entry file
  • Pin the exact version, or the exact commit for a git spec, and keep the lockfile
  • Install into one profile, and keep a clean profile you can boot when something breaks
  • Diff --dump-config before and after every install
  • Run the harness as its own unix user, on loopback, reached over SSH
  • Keep one API key on the box, and rotate it the day you remove a plugin you no longer trust

None of this is a reason to avoid plugins. The plugin model is why dsh is useful, and a harness you cannot extend is a harness you will replace. It is a reason to know what you installed, from whom, at which version, and to run the whole thing somewhere you can rebuild.

FAQ

Does dsh sandbox plugins from each other?

No. A plugin is loaded into the harness process through Cordis and can reach the documented capability seams, including shell, filesystem, web, subprocess, subagent and credentials. dsh-base, the first bundle in every profile, ships the sandbox and approval policy that governs what the agent's tools may do, and that policy is where your protection comes from. There is no per-plugin permission boundary, so the honest model is that installing a plugin extends your trust to its author and to every package in its dependency tree.

Can I install a dsh plugin without running its install scripts?

pnpm 10 and later block dependency build scripts by default, and dsh plugin ... add forwards to pnpm, so on a current pnpm the install does not run package scripts unless you approve that package. Confirm your version with pnpm --version. This does not make an unread plugin safe. The plugin's own code runs on the next boot because the harness loads it deliberately, which no install-time restriction affects.

Where do dsh plugins and their config actually live?

$DSH_HOME defaults to ~/.dsh. Profiles sit in $DSH_HOME/profiles/<name>, each holding a package.json with its plugin dependencies plus the dsh.profile manifest of ordered bundles, and a cordis.patch.yml patch layer. Installed packages land under $DSH_HOME/profiles/node_modules. Keys are in $DSH_HOME/.credentials.yaml, environment values in $DSH_HOME/.env, and a home-level $DSH_HOME/cordis.patch.yml applies over every profile. Run dsh --profile web --dump-config to see the composed result without booting.

Is it safe to install from the dsh plugin market?

The market restricts installs to sources on a curated registry and blocks build scripts unless you approve them per package, which is a real improvement over pasting a package name from a chat window. Its own README still states that listing is not endorsement, because the plugins are third-party code from other people. Read the source and pin the version. Keep the harness on a user account, and ideally a machine, you can afford to lose.