Saltar al contenido principal

DeepSeek Harness: the agent where the agent loop itself is a plugin

Avanzado

DeepSeek Harness — the command is dsh — is DeepSeek's open-source agent harness. It appeared on GitHub on 13 August 2026, carries 242,626 stars and 29,081 forks as of today, is MIT licensed and written in TypeScript. On 1 October 2026 DeepSeek added signed desktop installers, which is what put it back on the Hacker News front page.

Its one-line pitch is the repository description: "Everything is a Plugin." That phrase is on the README of roughly every agent framework ever published, and it almost always means "we have a plugin folder." Here it means something much stronger and much weirder, stated plainly in docs/architecture.md: the model adapter, the tool registry, the session log and the agent loop itself are plugins, "so each is replaceable from configuration." You can delete the agent loop from a YAML file and drop in your own without forking anything.

That design has consequences — some excellent, some expensive. This page is about the ones you cannot read off the landing page.

What you'll learn
  • Understand what 'everything is a plugin' buys you here, and what Cordis — the framework underneath — actually guarantees
  • Run your existing Claude Code hooks.json inside dsh, and know which subset survives the bridge
  • Point dsh at Claude, GPT or a company gateway, and know why an OAuth subscription will not work
  • Use PTC mode (run_code) deliberately instead of accidentally
  • Read the three sandbox modes against the kernel primitive each one really uses
  • Avoid the five configuration traps that cost an hour each

The 60-second version​

Guided walkthrough1 of 5
  1. `npx @deepseek-ai/dsh web` boots the web profile, prints a URL — `http://127.0.0.1:3080` — and opens your browser. No clone, no build. The directory you invoke it from becomes the default workspace root, but a fresh Web UI has no workspace selected until you add one, and the composer stays disabled until you do.

"Everything is a plugin" is a load-bearing claim​

Underneath dsh is Cordis, a plugin framework vendored into the repository (vendor/) and described in an arXiv paper the project cites — A Programming Paradigm for Spatiotemporal Composability, arXiv:2608.25512, submitted 26 August 2026, from authors at Peking University and DeepSeek-AI. The paper's two ideas map directly onto how the harness behaves at runtime:

  • Temporal composability — a component's side effects can be completely reversed when it is removed. In the harness this is ctx.effect(): prompt sections, tool schemas, model adapters, providers and listeners are all installed as reversible effects, so unloading a plugin unwinds its registrations in order rather than leaving orphans behind.
  • Spatial composability — dependencies resolve reactively. A plugin names the services it needs via inject, and Cordis simply does not start it until those services exist. There is no boot sequence to maintain; load order is a consequence of declared requirements.

A service claims a stable key on the shared context. The names are the architecture:

Service keyWhat owns it
ctx.llmthe LLM adapter registry — the model seam
ctx.toolsthe tool registry and its scoped execution pipeline
ctx.agentLoopthe default agent-loop driver
ctx.agentsthe live agent registry
ctx.sessionsthe append-only SessionEvent log
ctx.systemPromptsystem-prompt assembly
ctx.fsfilesystem access
ctx.subprocesssubprocess execution
ctx.sandboxsubprocess confinement
ctx.subagentssubagent delegation
ctx.ptcRuntimeprogram execution for run_code
ctx.credentialsthe credential seam

The repository calls these capability seams, and splits every one into three roles: the package that declares the service, the packages that implement it, and the packages that consume it. ctx.sandbox is declared by packages/sandbox/sandbox, implemented by sandbox-local and sandbox-windows-acl, consumed by tool-bash and the PTC runtime. Replace the implementation and every consumer follows, with no consumer edits.

Communication between plugins is typed events with five declared dispatch modes, and the mode is part of an event's public contract:

ModeAwaitedOrderReturns a value
emitnoregistration orderno
waterfallnoregistration orderyes
parallelyesall at onceno
serialyesregistration orderyes
bailnoregistration order, stops at first bailyes

waterfall is the interesting one: it is around-middleware. A listener gets (...args, next), calls next() to pass a possibly-wrapped result down the chain, or returns without calling next() to short-circuit. For single-decision events — "may this tool run?" — short-circuiting is the design: a policy listener that owns the decision returns without delegating, while a listener that merely annotates must delegate. If you have written a Claude Code hook that returns a deny decision, you already know this shape; the difference is that here it is the native extension mechanism rather than a shell-out.

The agent loop runs turns (zero or more steps) and steps (one model request plus its tool calls), and the published extension sequence is agent/pre-step → agent/request → llm/stream → tools/* → step/end. Those are the hook points. You do not patch the loop; you listen to it.

Three things that matter if you already use Claude Code​

1. It will run your existing Claude Code hooks.json​

This is the single most under-reported fact about dsh. packages/hooks ships three packages: hook-protocol (the shared engine), hooks-claude-code and hooks-codex. Point the Claude Code bridge at an existing hooks.json and your hooks run inside dsh when sessions start, prompts arrive, tools run and runs stop. The group README is explicit about what they can do: block prompts or tool calls with model-visible messages, add conversation context, or require the run to continue.

The limit is equally explicit, and it is the thing to check before you plan a migration: each integration supports only the command-hook subset documented by its source tool. So your command-type hooks port. Anything that depends on Claude Code behaviour outside that subset does not. If you have a hooks layer you trust — see Claude Code hooks — this is a genuine "bring your guardrails with you" path, not a rewrite.

2. You can point it at Claude — but not with your subscription​

The provider list shipped in the installed catalog includes anthropic and openai by name. For anything not in the catalog — a relay, a company gateway, a self-hosted server — the custom-provider form asks for a Provider ID, a base URL, a credential and an API protocol, and offers exactly three: OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages. In cordis.patch.yml these are stored as openai-completions, openai-responses and anthropic-messages.

Two consequences people trip over:

  • A provider speaks one protocol. A gateway that serves both OpenAI and Anthropic shapes needs two provider entries, not one.
  • OAuth sign-in is not supported. The providers guide states it directly, using Codex as its example: providers that sign in with OAuth "are not supported here yet." That means API-key billing, not a seat you already pay for. If you were hoping to drive dsh off an existing assistant subscription, budget for metered tokens instead — the arithmetic is in what AI costs across providers.

There is also a model-discovery button that asks your endpoint which models it serves. Useful, with a caveat worth internalising: a built-in provider is always answered from the installed catalog even when its base URL points at a gateway. So if you want to see what a gateway really serves, you have to add it as a custom provider. Otherwise you are reading DeepSeek's shipped list back to yourself.

3. run_code is codemode, and it is a first-class runtime​

packages/ptc-runtime is the capability family behind what the tool registry presents to the model as run_code. The contract: the model writes one program that calls host-provided functions as ordinary async calls, and only the program's printed output and return value come back. Not the intermediate tool results. Not twenty round-trips.

The specifics:

  • The TypeScript backend runs each program in a fresh managed Node process under the resolved sandbox policy.
  • Each run starts with no state from earlier programs. There is no implicit REPL session to lean on; carry state through files or the return value.
  • Failures come back as results, not exceptions, so a caller can diagnose them or hand them to the model.
  • An experimental Python backend exists, owning an fd-3 wire protocol between the Node host and a CPython subprocess, for when you genuinely need CPython.

If this pattern is new to you, the clearest write-up of why it saves so much context is on Pi 1.0 and the codemode trick: tool results that never enter the transcript cannot inflate it. The same logic applies here. The difference is that in dsh the program runtime is a swappable seam (ctx.ptcRuntime) rather than a fixed sandbox choice.

The sandbox: three modes, three kernel primitives, one honest caveat​

The three modes will look familiar if you have configured Codex:

ModeWhat a subprocess may do to files
read-onlyread only
workspace-writewrite only under the session workspace
danger-full-accessunrestricted

What is more interesting is the backend table, because it tells you what is really enforcing the policy on your machine:

  • Linux — bwrap, then Landlock as the fallback rung.
  • macOS — Seatbelt.
  • Windows — a restricted token, with sandbox-windows-acl as the write-restriction backend: confined children may write only in the workspace and a private temp directory.

A denied call can retry through a user-approved one-time escalation, so the policy is a prompt-and-proceed boundary rather than a hard wall.

And then the caveat, which the package README states itself and which you should quote to anyone who tells you this is "sandboxed": confinement is same-world only. It shares the host kernel and the host filesystem. Real isolation — containers, microVMs, remote executors — does not register here at all; it replaces whole capabilities instead.

SAFETY.md is unusually candid for a 242k-star repository, and worth reading before a first run rather than after. It says the harness can execute model-generated code, load third-party plugins, reach the network and touch processes, credentials and files; that there is no inherent guarantee that sandboxing, approval prompts and permission controls will prevent damage to anything reachable; and that the software "must not be treated as secure or production-ready." Its own recommendations are the boring correct ones: disposable VMs or containers, backups of anything inside the project scope, and no sensitive credentials unless you accept the risk. The same reasoning applies to any harness you run locally — see securing local and hybrid agents and sandboxing and credential masking.

There is also a small guard group worth knowing about: timeout-policy and repeat-tool-reminder — the latter being a nudge for the failure mode where a model calls the same tool in a loop.

The session log is the product​

dsh persists sessions as a versioned JSONL file — session.vN.jsonl[.zstd] — with migration chains between versions, through a ctx.sessionPersistence seam whose shipped implementation is session-persistence-jsonl. Committed events include turn/start, step/start, user/message, assistant/message, tool/result and turn/end.

The invariant behind it is the part to steal even if you never run dsh: "model-visible means logged." Every input the model saw must be reconstructable from the log. Live events exist separately, for driving UI in real time; they are explicitly not the record of truth.

That single rule is what makes the surrounding packages possible:

  • session-query-sqlite — query sessions out of SQLite instead of grepping JSONL.
  • dsh-session-projection with ctx.sessionProjections — fold session state incrementally rather than replaying from zero.
  • llm-replay plus token-meter ("replay token measurement") — replay a recorded session and measure its token cost, which is how you compare two prompt or model changes on identical inputs instead of on vibes.
  • compaction-tool-result-pruner — described as model-free tool-result pruning, i.e. shrinking the transcript without paying a model to summarise it. Compare with Claude's own approach in context compaction.

If you have ever tried to debug an agent run and discovered your logs recorded the rendered UI rather than the bytes the model received, you already know why this invariant is worth more than most features.

MCP here is a per-server plugin, not a global registry​

dsh speaks MCP over both stdio and Streamable HTTP, using the official SDK for negotiation, discovery, validation and cancellation. The design choices are unusual enough to list:

  • Servers are opt-in, one plugin entry per server. You configure one @deepseek-ai/dsh-mcp-client entry per server in the Cordis scope you want it visible in. There is deliberately no shared ctx.mcp service: the client is a connection plugin and a consumer of the tool registry.
  • No configured server means no MCP tax. A caller with no visible configured server receives no MCP prompt text and no MCP tools, in native or PTC mode. Zero bytes, not "a small section." That matters more than it sounds — see the real cost of MCP tools.
  • Public tool names include the server name, so identically-named tools from two servers stay distinct. A serverName must be unique inside its registration scope, but separate agent scopes may reuse it.
  • Server instructions join the logged system prompt, bounded by the client's maxInstructionBytes. A verbose server cannot quietly annex your prompt — but note that this is also the channel through which a malicious server speaks to your model. That is the attack surface described in MCP tool poisoning and rug pulls.
  • A failed tool-list refresh retains the previous tool generation rather than dropping your tools mid-session, and there is no product setting that forces a protocol revision — negotiation follows the SDK's supported revisions.

Configuration: five layers, and the override that eats your providers​

A profile directory holds a package.json — out-of-tree plugin dependencies plus a dsh.profile manifest with an ordered bundles list — and a cordis.patch.yml, your own patch layer. The composed tree is built over an empty root in this order:

Guided walkthrough1 of 5
  1. Bundles resolve from the dsh installation first — `@deepseek-ai/dsh-base`, `dsh-web-app`, `dsh-headless`, `dsh-sdk-app`, `dsh-sdk-minimal`, `dsh-acp-app` — then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. `dsh-base` is the shared floor: model adapters, tools, persistence, sandbox policy. A bundle declares itself through a `dsh` field in its `package.json`.

With HMR enabled in YAML, dsh-hmr watches the profile manifest and both patch files and recomposes every layer through one serialized reload. Without it, changes apply on restart.

Five gotchas that will cost you an hour​

  1. A Cordis config override replaces the complete entry config. The providers guide spells this out: when you edit an existing override, you must preserve the other fields and other providers yourself. Patch one provider carelessly and you have deleted the rest. This is the single most expensive mistake available in cordis.patch.yml.
  2. A Provider ID is permanent. Requests, saved sessions, model defaults and credential references all key off it. Renaming means adding a new provider and deleting the old one. Display name, base URL, protocol, credential and model list stay editable.
  3. --dump-config-schema imports plugin code. Unlike the other dump flags, it loads the composed tree's declared plugin schemas to print JSON Schema — so pointing it at untrusted plugins executes their import-time code. The CLI reference has a dedicated safety note on this. Treat it like reviewing third-party code, not like --help.
  4. Plugin version compatibility is enforced, and the escape hatch is explicit. Installation and profile startup check declared DSH peer ranges against the runtime version from dsh --version; an incompatible plugin requires an acknowledged exact-version exemption (allow-version, revoke-version). Good hygiene, surprising the first time a plugin refuses to load.
  5. A session that has already sent a request keeps the model recorded in its own log. Changing the default model does not retroactively move a live session, and if a saved default names a deleted provider the composer blocks input until you pick another. Both are correct behaviour; both look like bugs at 1 a.m.

Running it​

Start the Web UI, then a headless one-shot run — no clone, no build

# Web UI on http://127.0.0.1:3080, workspace defaults to the invoking directory
npx @deepseek-ai/dsh web

# One persisted session, final answer to stdout, then exit — the CI shape
npx @deepseek-ai/dsh --profile headless "summarize this repo and list its main packages"

# Serve automation clients over ACP stdio (editor integrations)
npx @deepseek-ai/dsh --profile acp

# Inspect the composed config tree WITHOUT booting it
npx @deepseek-ai/dsh --profile web --dump-config

# Manage a profile's plugins (forwards to pnpm inside the profile directory)
npx @deepseek-ai/dsh plugin --profile web add some-dsh-plugin

Point dsh at a gateway that speaks Anthropic Messages — in $DSH_HOME/profiles/web/cordis.patch.yml

# WARNING: a Cordis config override replaces the ENTIRE entry config.
# Include every provider you want to keep, not just the new one.
- id: llm-pi-ai
config:
  providers:
    my-anthropic-gateway:
      apiKeyEnv: GATEWAY_API_KEY
      api: anthropic-messages      # or openai-completions / openai-responses
      baseURL: https://gateway.example/v1
      models:
        - id: some-text-model
        - id: some-vision-model
          input: [text, image]     # omit to inherit the catalog's input types

Bring your Claude Code guardrails across

# The Claude Code hook bridge runs your existing command hooks inside dsh.
# Only the command-hook subset documented by Claude Code is supported.
# Hooks can: block a prompt or a tool call with a MODEL-VISIBLE message,
# add conversation context, or require the run to continue.
#
# Check the bridge's own README for the current config field names before
# wiring this up — the 0.2 line is a developer preview and they can move:
#   packages/hooks/hooks-claude-code/README.md
#
# Verify enforcement the same way you would in Claude Code: a hook that is
# configured but never fires is worse than no hook, because you trust it.

Where it sits​

If you are choosing a harness rather than studying one, the honest summary is: dsh is the most architecturally interesting open harness shipping right now and the least ready to be load-bearing. Its own safety file says so. The plugin model is genuinely stronger than its competitors' — ctx.agentLoop being replaceable is not a thing you can do in most harnesses at any price — and the hook bridges mean adopting it is not an all-or-nothing bet.

Against that: a release-candidate latest tag, no security audit, promised breaking changes, and a provider story that assumes API keys. For a comparison across the field, see coding agent CLIs compared and local coding agents. For the models you would most plausibly drive it with, see DeepSeek-V4.1-Flash.

Pulsa Intro o Espacio para girar la tarjeta. Usa las flechas izquierda y derecha para moverte entre las tarjetas.Término mostrado.
1 / 15

Check yourself

0/7
  1. In DeepSeek Harness, which of these is NOT replaceable from configuration?
  2. You want to drive dsh with Claude using the subscription you already pay for. What happens?
  3. You edit `llm-pi-ai` in cordis.patch.yml to add one new gateway, listing only that gateway under `providers`. What is the result?
  4. What does `workspace-write` actually rely on, on macOS?
  5. Why can dsh replay a session and measure its token cost?
  6. You configure no MCP servers. What does the model see?
  7. Which dump flag should you NOT point at untrusted plugins?

Sources & further reading​

  • deepseek-ai/deepseek-harness — the repository: README, star and fork counts, MIT licence, dsh-plugin topic.
  • docs/architecture.md — the plugin claim, service keys, profiles and bundles, the turn/step loop, the session-log invariant.
  • docs/cordis-primer.md — Cordis in five ideas, the five dispatch modes, waterfall semantics, loader configuration.
  • docs/capability-seams.md — the generated service graph: every ctx.* key, its owner, implementations and consumers.
  • SAFETY.md — what the harness can do to your machine, and the explicit "not secure or production-ready" statement.
  • packages/sandbox/README.md — the three modes, the per-platform backends, escalation, and "same-world only".
  • packages/hooks/README.md — the Claude Code and Codex hook bridges and the command-hook-subset limit.
  • packages/ptc-runtime/README.md — the run_code contract, fresh-process execution, the experimental Python backend.
  • docs/subsystems/mcp.md — per-server client plugins, scope rules, name namespacing, maxInstructionBytes, protocol negotiation.
  • docs/user/guide/providers.md — provider ids, the three API protocols, OAuth limitation, credential storage, the override warning.
  • apps/cli/README.md — entry modes, profile composition order, bundle names, dsh plugin, the dump flags, version exemptions.
  • DeepSeek Harness product page — the desktop installers (macOS Apple Silicon .dmg, Windows 64-bit .exe) and the preview status.
  • arXiv:2608.25512 — A Programming Paradigm for Spatiotemporal Composability, the Cordis paper the project cites.
  • awesome-dsh-plugin — community plugin index for the dsh-plugin topic.

Next​