How to read this guide
DeepSeek Harness is a pre-release project (version 0.1.1-rc.2, breaking changes expected) that moves fast enough for eight release candidates to ship in a week. Two of them landed
on the same day. Any document about it is a photograph, not a map, so this one states its shutter
speed: it describes commit b150a55 of 2026-08-21, and every factual claim carries a badge
saying how it was established.
Verified by executing the published dsh binary or inspecting the installed npm
artifact. The strongest evidence here.
Read from the repository at b150a55 but not executed. Reliable for structure;
weaker for runtime behaviour.
From outside the project - public registries, published commentary, other vendors' documentation.
Source-reading is weakest precisely where behaviour is concerned. A file can describe an intent that the running system does not honour. Where a claim in this guide is behavioural and carries only a Source badge, treat it as well-founded but unconfirmed, and check it before depending on it.
Contents
- 01What a harness is, briefly
- 02What DeepSeek Harness is
- 03"Everything is a plugin", qualified
- 04Bundle, Profile, Preset
- 05Host plane and agent plane
- 06The four presets that ship
- 07Composition and layer order
- 08Bringing your own model
- 09Writing a plugin
- 10What has already been built
- 11Security posture, honestly
- 12How it compares
- 13Commonly repeated errors
- 14Documentation drift at this commit
- 15How this was verified
What a harness is, briefly
A model produces tokens. Everything else that makes an agent useful is the harness: the tools it may call, the loop that decides when to stop, what goes into the context window and what gets evicted, which actions need approval, where the conversation is persisted, and what the process may touch on disk. The model is the engine; the harness is the rest of the car.
Three corrections to the usual framing are worth carrying into the rest of this guide. External
- A better harness does not always beat a better model. In one controlled comparison by Databricks, swapping harnesses changed cost by roughly 2x with quality essentially unchanged. The harness thesis is strongest for long-horizon, interactive work, not universally.
- Harnesses are decreasingly model-agnostic. Harnesses and models are increasingly post-trained together, so swapping the model under a harness tuned for another one tends to degrade it in ways that are hard to see.
- A harness is not an agent framework. LangGraph is a library for building harnesses. Claude Code is a harness. The distinction matters when comparing them.
This guide does not attempt a general introduction to harness engineering; that ground is well covered elsewhere, notably by walkinglabs/learn-harness-engineering. What follows is specific to DeepSeek Harness.
What DeepSeek Harness is
DeepSeek Harness (dsh on the command line) is an MIT-licensed, TypeScript agent harness
published by DeepSeek. It is built on a vendored fork of Cordis, a dependency-injection
and plugin framework, and its organising idea is that the harness should be assembled from plugin layers
rather than configured within a fixed application. Source
You launch it as a profile: an ordered stack of plugin bundles under your own overrides. The
documented entry point is npx @deepseek-ai/dsh web, which serves a local web UI on port
3080. There is also a headless profile for one-shot tasks, an ACP server for automation, and a JSON-RPC
SDK. It is web-first in a field that is mostly terminal-first. Run
Scale, stated accurately
The repository contains 227 packages, all named @deepseek-ai/dsh-*. That number is widely
quoted and it is nearly useless, because no deployment loads 227 packages. The number that describes
what actually runs comes from asking the binary: Run
| Profile | Composed rows | Disabled | Active plugins |
|---|---|---|---|
web | 129 | 25 | 104 |
headless | 81 | 2 | 79 |
dsh-base alone | 78 | 1 | 77 |
So a real web session boots 104 active plugins composed from 129 rows across two bundles. Of the 128 distinct packages in that tree, only two come from Cordis itself; the rest are harness packages. That is the honest measure of how much of this system is assembled rather than hard-wired.
There are no published harness-level benchmarks. The repository's
BENCHMARK.md is 231 bytes and three lines long; it explains how to run the Python SDK
against an example and contains no numbers at all. A question in the public launch thread asking
for harness-level rather than model-level benchmarks went unanswered.
Run External
A harness marketed on the strength of its architecture has, as of this commit, published no evidence that the architecture improves task outcomes. That does not make the architecture bad. Much of it is well built. But it should temper any claim that it is better rather than different.
Open source, but not open governance
CONTRIBUTING.md is unusually direct, and reading it changes how you should relate to the
project: Source
- External pull requests are not accepted - "We are sorry that we cannot accept external pull requests at the moment."
- The issue tracker is disabled. GitHub Discussions is the only channel for reporting bugs.
- Community plugins are explicitly not second-class: "We do not believe that packages in the official repository are inherently more important than packages created by the community... You may consider this repository an idea, an official showcase, and a source of inspiration, but not a mandate from us."
- The project asks for exactly two things from outsiders: publish plugins under the
dsh-pluginGitHub topic, and "write blog posts and how-to guides about DeepSeek Harness."
The result is a project that is source-available and freely licensed without being community-governed. That is a coherent position and the project states it plainly, but it is a different thing from the "open source" most readers will assume, and worth knowing before you build a business on it.
"Everything is a plugin", qualified
This is the project's signature line and the thing most repeated about it. It is an accurate description of the architecture's intent and a false literal statement, and the source says so. Source
There is a kernel. It consists of Cordis's Context, the root Fiber, and the
built-in Reflect, Registry, Events and Logger services, plus host bootstrap. Bootstrap then mounts the
Loader as a plugin, and from that point the generalisation holds - the model
adapter, the tool registry, the session log and the agent loop are all plugins. But some things are
deliberately not: Session, for instance, is a plain class rather than a service.
Nearly every capability above a small kernel is contributed through one common plugin lifecycle, and no capability above that kernel is privileged over a third-party replacement. That is a strong claim and it is substantially true. "Everything is a plugin" is shorthand for it.
Is the idea novel?
Deep plugin architectures are old: OSGi Declarative Services dates to 1999, and Eclipse extension
points, Emacs, VS Code, Spring and Guice all long predate this. Dependency-injected service registries
are not new, and neither is around-middleware. The Cordis paper concedes much of this itself,
noting that iPOJO's Gravity "directly prefigures Cordis's ctx.provide/ctx.get
pattern" - the authors' own word. External
What is claimed as novel is narrower and more defensible. The first is composition of inverses, where you write an inverse only for each atomic effect and the inverse of any composite is derived. The second is asynchronous teardown, which OSGi's synchronous deactivation callback cannot express. The contested question is whether that justifies calling it a programming paradigm.
The sharpest public critique comes from a direct peer, the author of a competing harness, who granted that per-registration cleanup handlers are nice but doubted the rest: the cross-plugin dependency injection "comes with a lot of footguns... most plugins do not have dependencies on each other, so this more complex system doesn't win you much." A substantive Chinese-language review reached a similar verdict: the architecture has novelty, the orchestration paradigm has no breakthrough. It also observed that "everything is swappable" does not automatically yield better task success rates. External
The Cordis paper argues that correctness becomes a structural property of the paradigm rather than a
matter of developer discipline. The repository's own vendoring log complicates that. Shipping
required 18 documented local patches to the framework, and the log is exhaustive by
policy. Item 6 "locally closes three reentrant disposal gaps" in
cordis/src/fiber.ts. Item 12 describes fixing "a deadlock that exited 13 with no
diagnostic." Run
That log is admirable engineering and admirable honesty. It is also evidence that these guarantees took a great deal of developer discipline to obtain.
Where the genuine novelty probably is
Not in pluggability, which is old, but in the agent modifying its own runtime. The
cordis preset exposes model-facing tools that let the agent define and mount a plugin into
the process it is currently running in. Every other harness lets you extend it; this one lets
the agent extend itself mid-session. Reversible-effect machinery is over-engineering for a
static plugin set, and becomes load-bearing the moment something is mutating the runtime while it
runs. This paragraph is inference, not a claim from the source.
Bundle, Profile, Preset
These three words are the most common source of confusion about dsh, and nearly every
secondary explanation of the project conflates at least two of them. Each is documented upstream; what
is not documented anywhere is the contrast, which is the reader's actual problem.
Run
| Concept | What it is | Who owns it | What ships |
|---|---|---|---|
| Bundle | An installable npm package that contributes one patch layer, declared by dsh.bundle.patch in its package.json |
The plugin author - this is what you publish | dsh-base, dsh-web-app, dsh-headless, plus the Codex and Claude Code subagent bundles |
| Profile | A directory at $DSH_HOME/profiles/<name> holding an ordered bundle list plus your own patch layer. Booted with --profile |
The user, maintained via dsh plugin |
web (base + web-app), headless (base + headless) |
| Agent preset | A per-session composition mounted under one agent's scope - the thing the UI calls a "mode" | The deployment, or you, by copying one | standard, code, minimal, cordis |
A bundle is what an external author distributes. A profile is what a user assembles and boots. An
agent preset is what one session sees. Nothing is both. A package without
dsh.bundle still installs, but contributes no layer and does nothing.
Here is a real profile manifest, generated by the tool rather than transcribed from documentation: Run
// $DSH_HOME/profiles/web/package.json
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]
}
}
}
Profiles are directories, and the shipped ones are created for you: asking dsh to dump
its configuration was enough to bring both web and headless into existence on
disk. An arbitrary new name is not auto-created. Until you run dsh plugin add
against it, it resolves to nothing and composes zero rows.
Host plane and agent plane
Once you have more than one session in a process, the question of where a plugin registers
becomes the thing that breaks. The shipped code preset explains this better than the
architecture documentation does, in a header comment on the file itself:
Run
"This file is an agent-plane composition. It is mounted under one agent's scope context, so every tool and prompt section it registers belongs to that session alone. The host composition keeps everything a preset must not own: the registries themselves, the sandbox and approval stack, persistence, and the model route."
So the split is:
Host plane
Process-wide and shared. The registries, the sandbox, the approval stack, persistence, the model route. Providers live here. A bundle patch mounts here.
Agent plane
Per-session and private. Tools, prompt sections, persona. An agent preset mounts here, and native sessions run beside each other in the same process, each seeing its own catalog.
From the same file: "A service row here MUST sit inside a group carrying an
isolate realm. Without one it publishes into the root realm, where it is process-global
rather than per-session and the second session mounting this preset collides with the
first; dsh-agent-presets rejects that at mount."
This is the single best "you will get this wrong" item in the product. It fails loudly rather than silently, which is the right design, but it will not be obvious why until you have read the sentence above.
The practical consequence for anyone building something real: a provider and its model-facing tool are two different roles that belong on two different planes. Mount the provider from a bundle patch on the host plane; mount the tool that consumes it from a preset on the agent plane. This is exactly how the shipped Codex subagent bundle is arranged.
The four presets that ship
Exactly four agent presets ship, discovered from directory names rather than declared in an
enumeration. The table below was read from the installed npm package - the artifact users run. A recursive search of that tree for orchestrator returns nothing.
Run
| ID | Name in the UI | Order | What it is |
|---|---|---|---|
standard | 标准模式 | 1 | Full coding agent: file editing, shell, file and web search, skills, plan, goals, subagents, workflows. The default. |
code | PTC 模式 | 2 | All of standard, with tools presented through the Code Mode SDK so the model composes multi-step operations in one TypeScript program. |
minimal | 极简模式 | 3 | A two-tool agent: persistent bash and str_replace_editor only. |
cordis | 创造模式 | 4 | All of standard, plus runtime inspection, plugin experimentation, and preset-authoring guidance. |
The UI names ship in Chinese only, including in the English package. An
English-speaking reader looking for "Code mode" in the interface will find PTC 模式
instead. The mode usually described as "creator" has the ID cordis and displays as
创造模式.
Upstream documentation is bilingual throughout, and a substantial part of this community is Chinese-speaking. An English-only guide reaches a fraction of it.
What Code Mode actually does
This is the most misdescribed feature in the project, so here is the shipped file's own header comment, verbatim: Run
"Everything in standard is here unchanged. What is added is the
tool-presentation row: instead of one tool call per action, the model writes a TypeScript
program against a generated SDK and run_code executes it, so a sequence that would be
five round trips becomes one... what this preset owns is the PRESENTATION of that
registry for this agent alone."
Code Mode is a tool-presentation change that collapses round trips. The program is an ephemeral call executed in a fresh worker with no cross-run state. It is not a durable artifact, and not a conversion of your session into a standalone program.
Composition and layer order
A profile composes over an empty root in a fixed order. Each layer is a list of patch operations
against rows identified by id: Source
- each bundle's patch, in
dsh.profile.bundlesorder - then the profile's own
cordis.patch.yml - then
$DSH_HOME/cordis.patch.yml - then any
--patchoverlays, in the order given
Later layers win per row, and a patch replaces a row's entire config. There is
no deep merge. If you patch a row to change one field, you must restate every other field
you want to keep. This is the composition footgun most likely to cost you an afternoon.
The best diagnostic in the product
--dump-config and --dump-default-config print the fully composed tree without
booting it, annotated with which bundle contributed each row and which bundle patched it.
No secondary explanation of this project mentions it, and it makes the layering model concrete in a way
no diagram does: Run
$ dsh --profile web --dump-default-config
# == @deepseek-ai/dsh-base
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
# == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app
- id: hmr
name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: [.]
disabled: true
It needs no API key and makes no model call, so it is the cheapest way to answer "what is actually loaded?" and the first thing to reach for when a plugin appears to do nothing.
Bringing your own model
Composition is abstract until you use it for something you actually want. For most readers that something is the model. A harness published by a model vendor invites one obvious suspicion: that the model connection is welded in. It is not. The verdict is more interesting than either "DeepSeek-only" or "run anything", and the gap between those two is where the practical detail lives.
The seam is genuine and the reach is wide, but breadth is configuration, not the shipped default. "Run any model" is marketing. "Run essentially every model that matters, after configuration" is accurate.
The seam is real
LlmAdapter is an abstract class in the LLM service, not a DeepSeek client with hooks
bolted on. Its only required method is stream(). Adapters are registered with
ctx.llm.registerAdapter(providers, adapter), and because registration is effect-owned,
an adapter disappears when its plugin unloads, exactly like any other contribution described in
section 03. Source
The service documents one obligation on adapter authors that is easy to miss: every provider HTTP
request must include attributionHeaders(). The two shipped adapters satisfy it through
completely different internals, one by direct fetch and one through a library header hook.
Source
| Package | What it is | Routes it registers |
|---|---|---|
llm/llm | The service and the neutral LlmAdapter base | None; it is the seam itself |
llm/llm-deepseek | First-party adapter, direct fetch | deepseek-official |
llm/llm-pi-ai | Multi-provider twin, delegates to pinned @earendil-works/pi-ai@0.82.1 | Zero, until settings supply profiles |
llm/llm-retry | Optional retry executor | Not a provider |
test-support/llm-replay | Deterministic replay for tests | Not a production network provider |
How far the catalog actually reaches
The llm-pi-ai adapter delegates to a pinned third-party catalog. Rather than repeat a
figure from someone else's summary, the count below was produced by calling
getBuiltinProviders() against version 0.82.1, the exact version the
repository pins. It returns 37 provider IDs: Run
amazon-bedrock, ant-ling, anthropic,
azure-openai-responses, cerebras, cloudflare-ai-gateway,
cloudflare-workers-ai, deepseek, fireworks,
github-copilot, google, google-vertex, groq,
huggingface, kimi-coding, minimax, minimax-cn,
mistral, moonshotai, moonshotai-cn, nvidia,
openai, openai-codex, opencode, opencode-go,
openrouter, qwen-token-plan, qwen-token-plan-cn,
together, vercel-ai-gateway, xai, xiaomi,
xiaomi-token-plan-ams, xiaomi-token-plan-cn,
xiaomi-token-plan-sgp, zai, zai-coding-cn
The Anthropic Messages, OpenAI Completions, and OpenAI Responses implementations behind these are real imported modules, not stubs. Source
Three limits worth knowing before you plan around this
The base composition mounts llm-pi-ai with zero routes and no extra
models in the picker, while separately setting the default to provider
deepseek-official, model deepseek-v4-flash. Routes register live only
once a llm-pi-ai: settings section supplies provider profiles, and they drop again
when that section empties. The comment in the base patch states the division plainly: which
adapters exist is composition, which providers run is the user's settings document. So the
out-of-box live route is DeepSeek-only, and every one of those 37 providers is a configuration
step away rather than a default. Source
If you declare a route yourself instead of picking a catalog entry, you get exactly
openai-completions, openai-responses, and anthropic-messages.
This is deliberate and the source explains why: Bedrock signs with SigV4 over AWS credentials and
a region, Vertex needs a project, a location and application-default credentials, Azure needs an
api-version, and Codex authenticates through OAuth. None of that fits a configuration shape of
key, endpoint and headers, so offering it would hand back a provider that cannot authenticate.
Catalog routes still reach every protocol through their own provider; only an explicit override is
refused. Source
Provider registration stores a retry policy, but llm/stream is a single-attempt call
wrapper. Retry is a separate optional plugin, dsh-llm-retry, loaded by the shared
example spine. Treat resilience as something you compose in, not something you inherit.
Source
What this means in practice
If you already pay for a frontier model, the useful reading is that anthropic,
openai, and github-copilot are catalog entries, so reaching them is a
settings exercise rather than an engineering one. If you run open weights locally, Ollama and most
self-hosted servers expose an OpenAI-compatible endpoint, which is precisely the
openai-completions protocol a hand-declared route supports. The community has already
built at least one adapter of its own, an OAuth-based GitHub Copilot provider, which is the clearest
available evidence that the seam is usable from outside the repository.
External
A protocol outside those three still needs a new adapter plugin. That is not a workaround; it is the intended extension path, and it is the same three-files-and-one-command exercise as section 09. An adapter is simply a plugin whose contribution happens to be a model route.
Everything above is read from source or executed against the pinned catalog. No model was invoked through any of these routes. This section describes what the code supports and what the shipped composition selects. It says nothing about latency, output quality, or how well any given model drives this agent loop.
Writing a plugin
An external plugin is three files and one command. No build step, no TypeScript toolchain, no bundler. The framework packages are peer dependencies supplied by the host at runtime, so plain JavaScript works. The sequence below was executed end to end. Run
1. package.json: the dsh.bundle key is what makes it installable
{
"name": "dsh-hello-probe",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
2. index.js: named exports only
export const name = 'hello-probe'
export function apply(ctx) {
ctx.logger('hello-probe').info('external plugin loaded')
}
3. cordis.patch.yml: name must match the package name exactly
- insert:
- id: hello-probe
name: dsh-hello-probe
4. Install it
$ dsh plugin --profile probe add ./dsh-hello-probe
dsh: initialized profile probe at $DSH_HOME/profiles/probe
+ dsh-hello-probe
Done in 223ms
dsh plugin ... add does more than install a dependency: it
automatically appends the package to dsh.profile.bundles. You do not
hand-edit the manifest. Note the resulting order: the new bundle lands after
dsh-base, so it patches base rather than being patched by it. Install order decides
layer order.
Three traps
Never also default-export
A function plugin uses named exports: name, optional inject, optional
Config, and apply. A service package instead default-exports its
class. Mixing the two makes the loader silently discard namespace metadata such as
inject, so your dependencies stop being awaited and you get a confusing
undefined-service crash.
Provider availability is not model access
Registering a web, skill, subagent or workflow provider does not give the agent a tool.
The matching consumer must also be in that agent's composition. A web provider needs
dsh-tool-web, a skill provider needs dsh-tool-skill, and so on. This is the
most likely reason a correctly written plugin appears to do nothing.
A bundle alone gives you no UI
A newly created profile composes base only. It boots cleanly and then sits there with no
interface, because an app bundle (dsh-web-app or dsh-headless) is what provides a surface. Add one, or install into an existing profile.
At the time of writing, the latest npm dist-tag for
@deepseek-ai/dsh-tools points at 0.0.1-rc.1, the first release ever published, while the current line ships under the next tag. A plain
npm install @deepseek-ai/dsh-tools therefore resolves to a stale package whose own peer
dependencies are an entire generation behind. Pin explicitly, or install @next.
External
Where you can attach
The documented extension seams, each consumed by declaring inject and calling a
registration method that returns a disposer: Source
- Tools:
ctx.tools.register(defineTool({...})) - Tool policy: the
tools/pre-execute,tools/execute,tools/post-executeandtools/resultwaterfalls - Web: search and fetch providers
- Skills: a discovery provider, or one runtime skill
- Subagents: implement a provider, expose it with a configured delegation tool
- Workflows: an engine, plus observation events
- Session events: declaration-merge your own durable payloads
- Compaction: provide or consume the engine
- Context: dynamic request context, turn-specific injection, and system-prompt sections
- LLM adapters:
ctx.llm.registerAdapter(...)
Two rules govern all of them. Every registration is an effect - it returns a disposer,
and disposal must remove the contribution. Waterfall listeners must call
next() to delegate; returning without it short-circuits the chain.
What has already been built
The most striking thing about this project is not its architecture. It is how fast people built on it. As of 2026-08-24, 11,258 GitHub repositories carry the dsh-plugin topic,
on a harness still at 0.1.1-rc.2. That is a remarkable community response, and it is also
a number that needs careful handling. External
A two-stratum audit of 200 of those repositories (100 by stars, 100 drawn deterministically from the recently-updated window) found 81 native plugins and 22 verified multi-tool integrations. The rest either showed no plugin signal or could not be determined from a root manifest.
Some of that is ordinary topic drift rather than anything untoward: reactive-resume
(41,636 stars, a resume builder), PicGo (27,028, an image uploader) and
nocobase (23,811, a no-code platform) all carry the topic. And roughly a quarter were
undetermined simply because the plugin sits in a monorepo subdirectory rather than at the root, so
the true count is certainly higher than 81. Browse the topic to discover plugins; do not
cite its count as a census.
Detail matters less than the shape, which is unambiguous: this is a real, active ecosystem with genuine traction. A sample of what has shipped: External
| Project | Stars | What it does |
|---|---|---|
nexu-io/open-design | 90,972 | Design workspace with native DSH support alongside Claude Code, Codex and Cursor |
Nagi-ovo/voyager | 19,822 | Browser enhancement and prompt manager across DSH and other AI web UIs |
DSH-better-sidebar | 2,792 | VS Code-style sidebar: files, terminal, Git, browser, chat, subagent panels |
dsh-TUI | 2,434 | Terminal UI with live status, streaming reasoning, rollback, context metrics |
dsh-market | 2,155 | In-product marketplace for browsing and installing plugins |
Tencent/BrowserSkill | 1,294 | Logged-in browser control, with a nested native DSH plugin |
dsh-context | 990 | Context composition, compaction, pruning and a usage dashboard |
dsh-im | 752 | Bridges DSH to Feishu, WeChat, DingTalk, Slack, Telegram, Discord and more |
Two entries deserve attention beyond their star counts. weijiafu14/pi2dsh is an
ABI compatibility layer that runs another harness's plugins inside DSH - cross-harness
plugin portability already exists in the wild. And lujianjun19/dsh-llm-github-copilot
is a GitHub Copilot OAuth adapter for the LLM seam, which is a concrete demonstration
that the model layer really is replaceable by a third party.
Where the gaps are
If you are deciding what to build, the useful output of that audit is the distribution rather than the total. Categorised across the sample:
Saturated - pick a different problem
Cost and token widgets (at least six independent implementations), themes and UI beautification (at least six), plugin marketplaces (three), memory layers (at least five). These are the obvious first ideas, which is exactly why they are crowded.
Contested but not closed
Vision tooling (two strong entries), context and compaction (one at 990 stars plus others), trajectory and run analysis (three). Room exists, but you would be competing on quality against something that already works.
Genuinely thin
Research workflows - the sample turned up one mathematical-research adaptation and one academic-writing auditor. Note the caveat in section 11 before building anything that fetches from the open web.
Empty
Enterprise and line-of-business. No ERP, CRM, Dynamics 365, Power Platform, SAP or Salesforce integration appeared anywhere in the sample. The nearest neighbours are a Chinese e-invoicing toolkit and an interview trainer. This is the largest unoccupied category found.
Stratum one was the top 100 by stars - what a browsing user actually sees. Stratum two took all ten
available pages of sort=updated, deduplicated to 947 candidates, and selected 100 by
sorting on a salted SHA-256 of the repository name, so the draw is reproducible rather than simply
"the most recent". Each repository was tested for dsh.bundle or
dsh.profile keys, a @deepseek-ai/* dependency, or a root Cordis manifest.
The two strata disagreed sharply: 12% native among the most-starred, 69% among the recently-updated. So no ecosystem-wide total is offered here. The 200-repository result is solid; anything extrapolated from it would not be.
Security posture, honestly
The project documents its own limits unusually well, but does so scattered across a dozen package READMEs. Consolidated, the picture is this, and it is the section to read before letting anything autonomous loose inside a corporate network. Source
The strongest boundary is OS-enforced file-write confinement for wrapped subprocesses. It is not network isolation, not credential isolation, not host-read isolation, and not plugin isolation.
| Boundary | What it constrains | What it does not |
|---|---|---|
| Confined subprocesses | Wraps exact subprocess arguments in an OS mechanism - bwrap then Landlock on Linux, Seatbelt on macOS, a restricted-token runner on Windows. On Linux, failure to obtain a usable mechanism fails closed. | Same-host-kernel confinement, not a container or VM. danger-full-access bypasses it entirely. |
| Network | Nothing. Network is not part of the policy vocabulary at all, and bwrap does not unshare the network. | Any confined process can still reach anything the host can reach. |
| Reads | Nothing. The policy vocabulary constrains writes. | Reads are unrestricted, including of credential files in your home directory. |
| Credentials | Child environments scrub DSH_* and variables whose names look like KEY, PASSWORD, SECRET or TOKEN. Code Mode gets an empty environment. |
HOME, proxy settings, normal CLI configuration, readable credential files and differently named secrets all survive. This is not a credential firewall. |
| macOS specifically | An allow-by-default Seatbelt profile with file-write* denied, then workspace and temp roots allow-listed. |
Depends on the deprecated sandbox-exec, and relies on launch-time failure rather than probing availability first. |
| Windows specifically | A restricted token plus per-workspace and per-session temp ACL grants. | Reported by the project itself as partial: Everyone-granted objects and NTFS hard links weaken path-based write isolation. |
| Code Mode worker | Fresh worker per run, empty environment, heap/output/compute/wall limits, message validation, hard termination. | Described in its own README as "containment, not a security boundary." It is a worker thread in the host process, and any OS process it spawns can outlive it. |
| Plugins | Dynamically defined packages get a separate VM realm with withheld globals and a guarded context façade. | Ordinary plugins execute directly in the host process. The source states host-realm helpers permit escape and that this is not a malicious-code boundary - treat it as equivalent to shell access. |
The HTTP fetch provider's own README states that it is "an SSRF primitive" and "must not be enabled in a deployment that can reach sensitive internal network targets." Combined with network being absent from the sandbox policy, that is a specific and serious constraint for anyone running this inside a corporate network. The web server package separately ships with no TLS, authentication or origin policy.
Two further caveats worth carrying. Installing a plugin from a git source requires granting permission
to execute package code at install time, outside any sandbox. And the minimal preset wires
its editor to the unsandboxed filesystem service, so its edits are governed by tool logic and account
permissions rather than the session's workspace mode - which contradicts the blanket statement in the
CLI reference that filesystem mutations are restricted.
None of this is unusual for the category. Every honest harness documents where its boundary leaks. One competitor notes its sandbox "does not run the CLI itself in a sandbox", another that its DNS-rebinding defence "does not eliminate" the risk. The failure mode to avoid is assuming "sandboxed" means "safe." External
How it compares
Positioning against the harnesses most readers already use. Licence and capability facts are from each vendor's own documentation. External
| dsh | Claude Code | Codex CLI | Copilot CLI | |
|---|---|---|---|---|
| Source open | Full | CLI closed | Full | No source |
| Licence | MIT | Proprietary | Apache-2.0 | Bespoke |
| Language | TypeScript | TS -> binary | Rust | Node |
| Extensibility | Plugins at every layer | Hooks, skills, plugins, MCP | Hooks, skills, plugins | Hooks, plugins, skills |
| Config format | YAML patch layers | JSON | TOML | JSON |
| Sandbox | Landlock/bwrap, Seatbelt, Win ACL | Seatbelt, bwrap | Seatbelt, Landlock, native Win | OS + cloud |
| Sub-agents | Including Codex and Claude Code as providers | Yes, plus agent teams | Yes | Yes |
| Persistence | Append-only event log; fork/resume | JSONL; resume/fork | JSONL -> SQLite | SQLite |
| Primary surface | Web, plus CLI, headless, ACP, SDK | Terminal, IDE, desktop | Terminal, IDE | Terminal, ACP |
| Owns the model | Yes | Yes | Yes | Broker |
Three things that table understates
It ships wrappers for two competitors, with pinned versions. The subagent provider
bundles depend on @anthropic-ai/claude-agent-sdk and @openai/codex, the
latter invoked as app-server --stdio. "Keep your existing tooling and point it here" is
not a metaphor; it is two npm dependencies. Source
Harness lock-in is being competed away by the vendors themselves. Copilot CLI reads
AGENTS.md, CLAUDE.md and GEMINI.md; Codex sets a Claude plugin
root variable; dsh ships bridges that execute both Claude Code and Codex hook files. The
convergence on AGENTS.md is real and it flows through the closed players too.
"Public repo" is not "open source" is not "open governance." Claude Code's public
repository contains no CLI source. Copilot CLI's licence permits running and unmodified redistribution
but forbids derivative works. dsh is MIT-licensed and complete, and accepts no external pull requests, with issues disabled. Three different meanings of "open", and the distinction
is worth making explicitly whenever someone compares them.
Commonly repeated errors
These circulate widely in secondary explanations of the project. Each correction below is cited to the shipped artifact or the source. No individual is named: the point is the correction, not who made it, and several of these are compressed summaries rather than mistakes.
The preset IDs are standard, code, minimal and
cordis. There is no orchestrator. A recursive search of the shipped
preset tree returns nothing. "Creator" describes cordis but is not its ID, and the
display names are Chinese.
Verified against config/agent-presets/ in the installed npm package. Run
It changes tool presentation. The model writes a program against a generated SDK and
run_code executes it, collapsing what would be five round trips into one. The program
is ephemeral, runs in a fresh worker with no cross-run state, and is not saved as an artifact.
The shipped preset's own header comment states this. Run
There is a kernel: Cordis's context and root fiber, plus built-in reflect, registry, events and logger services and host bootstrap. The Loader is then mounted as a plugin. Some objects are deliberately plain classes rather than services. The accurate claim is that nearly everything above a small kernel is a plugin, and nothing above it is privileged.
The repository's own instructions call the phrase shorthand. Source
Trajectory is a turn-aware event ledger. It renders provenance for sourced context, including a plugin kind, but assistant and tool records do not generally carry their owning plugin, so it cannot attribute every loop action to an originating plugin.
Read from source, not executed - treat as well-founded but unconfirmed. Source
Network is not in the sandbox policy vocabulary, reads are unrestricted, the environment scrub is not a credential firewall, and both Code Mode and dynamically defined plugins are documented as containment rather than security boundaries. See section 11.
Consolidated from package READMEs and sandbox sources. Source
227 is the repository's package count. A real web session composes 129 rows and boots
104 active plugins; headless boots 79. Ask the binary with
--dump-config rather than counting directories.
Measured from composed profile output. Run
Documentation drift at this commit
Four inconsistencies between the documentation and the tree, current as of b150a55. A
project shipping this much this fast having four stale references is unremarkable; they are listed
because each one will waste someone's afternoon, not as criticism. Run
| Where | What it says | What is true |
|---|---|---|
packages/README.md |
Documents 48 package groups | Two groups exist but are undocumented: mcp and runtime-diagnostics |
AGENTS.md |
Lists a self-modification/ directory |
No such directory. The code lives in packages/extensions/ |
Plugin tutorial and CLI reference, both languages, plus the binary's own --help |
Cite deepseek-harness/turtle-ui as the working external plugin example |
That repository does not exist. The organisation has zero public repositories. It is the only third-party example named anywhere |
docs/cookbook/adding-a-package.md |
Lists private: true as an enforced package.json invariant |
The constraints gate raises "release member must not set private: true" for every packages/<group>/<pkg> outside experimental/ - the advice holds only for experimental packages |
Separately, and with more practical consequence: the latest npm dist-tag for
@deepseek-ai/dsh-tools points at the first release ever published, while current versions
ship under next. Anyone following the plugin tutorial's install instruction gets a stale
package. External
Because the project accepts no external pull requests and has issues disabled, the only route for any of this is GitHub Discussions.
How this was verified
The part of this document that should determine how much you trust the rest.
What was executed Run
- Installed the published
@deepseek-ai/dshCLI from npm into an isolated prefix with a privateDSH_HOME, and confirmeddsh --version. - Dumped the composed configuration for the
webandheadlessprofiles, and counted rows, disabled rows and distinct packages. - Read the shipped agent-preset directory from the installed package, including every
preset.ymland thecodepreset's composition file. - Searched the shipped preset tree for
orchestrator, finding nothing. - Wrote a three-file external plugin, installed it with
dsh plugin add, confirmed it was appended to the profile's bundle list and appeared in the composed tree, and booted the profile. - Confirmed the repository HEAD, the absence of
packages/self-modification/, the length ofBENCHMARK.md, and the 18 numbered entries in the vendoring log. - Queried public registry metadata for the dist-tag discrepancy, cross-checked against two independent sources.
- Called
getBuiltinProviders()against@earendil-works/pi-ai@0.82.1, the version the repository pins, and counted the 37 provider IDs listed in section 08.
What was read but not executed Source
- The entire security posture in section 11. Every statement there is sourced from package documentation and sandbox source, and none was tested by attempting to escape a sandbox.
- The trajectory-provenance correction in section 13.
- Composition layer order and the no-deep-merge behaviour of patches.
- The extension-seam inventory in section 09.
- The LLM adapter contract, the dormant multi-provider mount in the base composition, and the three-protocol limit on hand-declared routes in section 08. No model was invoked through any route.
Known limitations
The binary exercised was 0.1.0-rc.7, not the checkout's 0.1.1-rc.2.
A registry mirror on the machine used for this work served a stale view and the current release could
not be installed from it. Row counts in particular may differ on the newer version. The structural findings (the preset inventory, the composition mechanism, the plugin install path) are unlikely
to have moved, but they have not been re-confirmed on the current release.
No model was invoked at any point. Everything here is keyless, which means nothing in this guide describes how the agent behaves in a real task, only how the system is assembled. Tool-calling behaviour, loop dynamics, compaction quality and output quality are all outside what was tested.
During this work a corporate npm mirror silently served a two-week-old view of the registry, which produced three confident and wrong conclusions about release timing before being caught by cross-checking against independent sources. Those claims were retracted rather than quietly adjusted. If you are verifying anything about what is published, do not trust a single mirror.