DeepSeek Harness: An Open-Source Agent Framework Where Literally Everything Is a Plugin
A Full Agent Stack, Dropped on GitHub
DeepSeek dropped a developer preview of DeepSeek Harness (dsh) on August 13, 2026. The repository lives at deepseek-ai/deepseek-harness. By end of day it had 1.6k stars, 12,293 commits, and 19 contributors — numbers that tell you this isn’t a weekend hack. The codebase is 97.1% TypeScript, with 1.6% CSS and 0.7% Python. Package version at publish: 0.1.0-rc.5.
The landing page is at deepseek.com/harness and the developer docs live at deepseek-harness.github.io/deepseek-harness.
Licensed under MIT.
The One-Line Pitch
From the README: “Agent = Model + Harness.”
The model is the soul. A harness lets an agent understand its environment, use tools, and keep working in real-world settings. DeepSeek’s take: every capability — models, tools, skills, sessions, sandboxes, storage, loops, scheduling, and the UI — is a swappable plugin.
This isn’t a slogan. It’s enforced by the architecture.
Cordis: The Kernel Under Everything
DeepSeek Harness is built on top of Cordis, a vendored plugin framework. Cordis’s design is described in a paper called A Programming Paradigm for Spatiotemporal Composability.
The whole framework reduces to five ideas:
- A plugin is an object that implements Service. It can be a function with
injectandapply(ctx), or aServicesubclass. - A context is a repository of services. A plugin claims a stable key like
ctx.tools,ctx.llm, orctx.sessions. Other plugins find services by key instead of importing concrete implementations. - Declare service dependency via
inject. A plugin that names required services waits until those services exist. No manual boot sequencing. - Typed Events for communication. Four dispatch modes:
emit(fire-and-forget),waterfall(middleware-style withnext()),parallel(all listeners run concurrently), andserial(listeners run in order with return values). - Registrations are reversible effects. Prompt sections, tool schemas, adapters, listeners — everything installs through
ctx.effect()so reload and teardown unwind them predictably.
The Cordis waterfall model is around-middleware. A listener receives (...args, next). Call next() to delegate to the next service; return without next() to short-circuit. For single-decision events, short-circuiting is the design — a policy listener can own a decision outright.
Everything Is a Plugin (Seriously)
The plugin list on deepseek.com/harness spells out the full capability surface:
- Models — the LLM backends (DeepSeek, OpenAI, Anthropic, Bedrock, Vertex, Azure, Codex, plus any OpenAI-compatible endpoint)
- Tools — what the model can call
- Skills — reusable prompt and tool bundles
- Sessions — conversation state management
- Sandboxes — where code runs (PTY bash,
danger-full-accesslocal backend, landlock-based native sandbox) - Storage — session persistence
- Loops — the agent turn logic
- Scheduling — subagent and task dispatch
- UI — the Web UI served at port 3080 by default
A developer can select, swap, or extend any of these via configuration — no source changes to DeepSeek Harness itself. The Plugin Config Catalog is auto-generated from the source (scripts/gen-config-catalog.ts) and verified fresh by CI, so every field in a cordis.yml config: block matches a plugin’s declared config type exactly.
Four Runtime Modes
The docs ship four presets. Each one targets a different use case.
Standard Mode — Full coding agent: file editing, shell, file and web search, skills, planning, goals, subagents, and workflows. This is the default Web UI experience.
Code Mode — Everything in Standard, but tools are exposed through a Code Mode SDK so the model can combine multi-step operations in one TypeScript program. One model-generated program replaces several tool-call rounds.
Minimal Mode — Just two tools: a persistent bash shell and str_replace_editor. This is for benchmarking models in a stripped-down environment. No skills, no planning, no subagents — raw model capability against a real codebase.
Creator Mode — Built for writing custom agent presets. Includes all Standard mode capabilities plus runtime inspection, in-memory Cordis plugin experiments, and preset-authoring guidance. This is how you build the fifth, sixth, seventh mode.
Every Run Is Traceable
This is where Harness distinguishes itself from most agent tooling. Everything the model sees is recorded in an append-only session log:
- System prompts
- Reasoning output
- Tool calls and their results
- Subagent scheduling decisions
- Every context injection
The Web UI has a Trajectory view where you inspect these records filtered by source. Resume, fork, search, and replay — all four operations work against the same event stream. No separate trace database, no export step. The session JSONL is the source of truth.
For the Python SDK, the session directory stores uncompressed JSONL containing the assembled model requests and tool calls. The example at examples/jsonrpc-agent/minimal.py shows this end to end.
Model Configuration: DeepSeek + Anything
The model configuration page (Configure models) has three layers.
DeepSeek Official. Open Settings → Models, paste a DeepSeek API key, save. The key is write-only. After saving, the UI receives a redacted descriptor, never the plaintext. Keys live in \$DSH_HOME/.credentials.yaml; the settings page only keeps a credential reference.
Catalog providers. Click “Add provider”, pick Anthropic or OpenAI, paste the key. Providers using native auth (Bedrock via AWS creds + region, Vertex via ADC project, Azure via api-version, Codex via OAuth) don’t work with just an API key field — each needs its own auth path.
Custom providers. For a company gateway, self-hosted server, or any provider not in the catalog. Set a lowercase Provider ID (permanent — requests, saved sessions, model defaults, and credential refs all use it), base URL, API protocol, credentials, and at least one model.
Visual models need one extra step. Since a custom endpoint has no way to advertise its supported modalities, the form can’t auto-detect vision support. You add input: [text, image] to the model in \$DSH_HOME/settings.yaml. If all your custom models accept images, set defaultInput: [text, image] once at the provider level instead of per-model. The input field is an assertion, not a check — if you claim a model does vision but the endpoint actually doesn’t, the provider rejects the request instead of Harness.
Troubleshooting is spelled out directly in the docs:
MISSING_CREDENTIAL→ store the provider key through the Models page or export the referenced env varUNKNOWN_MODEL→ pick a configured model, or add the missing model to the custom provider- “Get available models returns 401” → check the key. Model discovery calls the OpenAI-compatible
GET /modelsendpoint. If your service doesn’t expose that, enter models manually. - “Image rejected before send” → the model didn’t declare
imagemodality. Addinput: [text, image]. - “Provider rejects a request with an image” → the model claimed vision capability its endpoint doesn’t actually have. Remove
imagefrom the list and start a fresh session (the old image stays in the session log and keeps repeating the same request).
Getting Started: Three Paths
Path 1: npx @deepseek-ai/dsh web
Install Node.js, run one command. The Web UI starts at http://127.0.0.1:3080. That’s it. No clone, no build, no pnpm.
npx @deepseek-ai/dsh webThen: Settings → Models → paste DeepSeek API key. Choose workspace (the directory where dsh was invoked works). Start a session:
Summarize this repository and identify its main packages.
The agent reads and edits workspace files, runs commands, delegates work, and maintains a plan. Any operation that needs approval under the active permission policy pops a dialog in the Web UI before executing.
Path 2: Clone and Build From Source
git clone https://github.com/deepseek-ai/deepseek-harness.gitcd deepseek-harnesspnpm installpnpm run buildpnpm dsh webPath 3: Python SDK
Requirements: Python 3.10+, Linux x64 / Linux arm64 / macOS 14+ on arm64, a DeepSeek-compatible endpoint.
git clone https://github.com/deepseek-ai/deepseek-harness.gitcd deepseek-harnesspython -m venv .venv. .venv/bin/activatepython -m pip install deepseek-harness-sdkSet credentials:
export DEEPSEEK_API_KEY=sk-your-key-here# Optional:# export DSH_MODEL=deepseek-v4-flash# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.'Run a task against an isolated workspace:
python examples/jsonrpc-agent/minimal.py \ --workspace /absolute/path/to/workspace \ --session-root /absolute/path/to/sessions \ --session-id example-001 \ "Inspect the repository and fix the failing tests."For your own code, the SDK entrypoint is DeepSeekHarness as a context manager:
from pathlib import Pathfrom deepseek_harness import DeepSeekHarness
config = Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()workspace = Path("/absolute/path/to/workspace").resolve()sessions = Path("/absolute/path/to/sessions").resolve()
with DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", max_tokens=49_152, cwd=str(workspace), session_root=str(sessions), cordis=str(config),) as harness: result = harness.run( "Inspect the repository and fix the failing tests.", session_id="example-001", ) print(result.final_response)DeepSeekHarness lazily starts the bundled runtime and reuses it until the with block exits. Reuse the same session ID to preserve the Bash process (working directory, env vars, shell functions). Use a fresh session ID for an independent task.
The jsonrpc-agent minimal composition is deliberately sparse: only persistent bash and str_replace_editor as model-facing tools. Bash timeout 300 seconds. Editor output limit 16,000 characters. Context compaction disabled. Filesystem uses the bare local backend — editor paths can address anything the runtime process can see. The docs warn explicitly: “Run it only inside a disposable checkout or container.” The persistent PTY backend also requires a POSIX terminal substrate — no Windows support for this composition.
Write Your First Plugin
Tutorial: Your first plugin. A plugin is a TypeScript module that exports an apply function:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) { console.log('[hello-plugin] plugin loaded!')}Register it in a cordis.yml patch:
- insert: - id: hello name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'Boot with the overlay:
pnpm dsh web --patch ./scratch-plugin/cordis.ymlAutomatic cleanup is the killer feature. Anything registered through ctx — event listeners, tools, timers — is cleaned up when the plugin unloads. No manual removeListener or clearInterval. For explicit cleanup (network connections), return a disposer from ctx.effect().
Dependencies are declared with inject:
export const name = 'my-tool-plugin'export const inject = ['tools']
export function apply(ctx: Context) { // ctx.tools is ready here}Cordis waits for every required service before loading the plugin.
Three plugin forms exist: function (above), object with apply, and class extending Service. Use class form when the plugin itself provides a service for other plugins to consume.
Write Your First Tool
Tutorial: Build a tool. Use defineTool from @deepseek-ai/dsh-tools:
import type { Context } from '@deepseek-ai/cordis'import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'export const inject = ['tools']
export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'greet', description: 'Greet someone by name.', parameters: { name: { type: 'string', required: true, description: 'The name to greet', }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { return `Hello, ${args.name}!` }, }))}defineTool infers and validates args from parameters. execute returns the canonical value declared by output.schema. output.render converts that canonical value into model-facing content. After a restart with the patch, ask the Web UI: “Use the greet tool to greet Ada.” The model calls greet and receives Hello, Ada!.
The next steps from the tutorial are plugin configuration, the tool authoring reference (nested schemas, canonical values, background work, policy hooks, Code Mode, UI cards), and capability layering (Service Definition → Service Provider → Consumer package split).
CLI Entry Modes
The @deepseek-ai/dsh command is the product launcher. Four entry points:
| Command | Purpose |
|---|---|
dsh --profile <name> |
Boot the named profile under \$DSH_HOME/profiles/<name> |
dsh --profile headless "job" |
Run one fresh persisted session, print the final answer, exit |
dsh web |
Alias of --profile web |
dsh plugin --profile <name> <pnpm args> |
Manage a profile’s plugins by forwarding to pnpm |
The invoking directory is the default workspace root. The web and headless profiles auto-initialize from shipped templates on first use. Any other profile must be created through dsh plugin.
Launcher flags come first. The first token the launcher doesn’t recognize becomes the app’s arguments. Example: dsh --profile web --port 8080 hands --port 8080 to the web app, not the launcher.
A profile directory holds a package.json (out-of-tree plugin dependencies, plus the profile manifest dsh.profile with its ordered bundles list) and a cordis.patch.yml (the user’s own patch layer). Composition order over an empty root is: each bundle’s patch in dsh.profile.bundles order → the profile’s cordis.patch.yml → the home-level \$DSH_HOME/cordis.patch.yml → --patch overlays. Use --dump-default-config and --dump-config to inspect the composed tree without booting it.
Community Plugins and Ecosystem
Tag your plugin repo with dsh-plugin on GitHub for discoverability. The official site links directly to that topic page. There’s also a DeepSeek Harness Discord community for discussion.
The project uses GitHub Discussions for feedback and bug reports. The docs link to CONTRIBUTING.md for development workflow, architecture.md for system design, and AGENTS.md for agent-specific coding conventions.
Developer Preview — Yes, It Will Break
The README puts this in ALL CAPS: “THERE WILL BE COMPATIBILITY-BREAKING CHANGES.”
Core plugins and APIs are still evolving. The landing page says it directly: “DeepSeek Harness remains in developer preview and is still being tested by developers building agent harnesses.”
If you’re building on top of it, pin a commit hash, keep your plugins thin against the config catalog, and expect to re-test on every rc bump. The plugin auto-cleanup and dependency injection make re-testing less painful than monolithic frameworks, but “developer preview” means exactly what it says.
What Makes This Different
Most agent frameworks today start with a turn loop and bolt on extensibility as an afterthought. Harness flips it: the extensibility is the framework, and the turn loop is just another plugin. The observable result is three things you don’t usually get in one package:
- Swap anything without a fork. Don’t like the built-in tool system? Replace it. Want a different LLM routing layer? Swap the provider. Everything resolves through Cordis service keys.
- Traceability by default. The append-only log isn’t an observability add-on. It is how sessions work. Resume, fork, search, and replay all use the same stream.
- Composable presets, not feature flags. The four modes (Standard / Code / Minimal / Creator) are just ordered plugin-bundle patch layers. You can layer your own presets on top with YAML instead of code.
Whether this approach wins over the monolithic SDKs depends on whether the plugin ecosystem produces enough third-party tools and providers to make the swap/recompose story real. At 1.6k stars and 12k commits on day one, the momentum is clearly there.
References
- DeepSeek Harness landing page
- deepseek-ai/deepseek-harness on GitHub
- Developer docs quickstart
- Model configuration guide
- Python SDK guide
- Your first plugin tutorial
- Build a tool tutorial
- Cordis Primer
- Plugin Config Catalog
- CLI README
- Cordis on GitHub
- Cordis paper: A Programming Paradigm for Spatiotemporal Composability
- Community dsh-plugin topic on GitHub
- DeepSeek Harness Discord