DeepSeek Harness: Inside the Open-Source Claude Code Rival (Part 1)
Series: Inside the DeepSeek Harness — Part 1 of 7
Today, DeepSeek released two things at once: DeepSeek-V4-Pro, an updated flagship model, and DeepSeek Harness (dsh) — a full, open-source, MIT-licensed agent harness, live on GitHub at deepseek-ai/deepseek-harness. The press framing arrived within hours and it wasn’t subtle: VentureBeat called it an “open source rival to Claude Code”; Decrypt’s headline was “DeepSeek Is Building Its Own Claude Code. Beijing Wants the Whole Stack.”
Here’s the part that makes this more than a routine model-and-tools drop: DeepSeek raised V4-Pro’s API price at the same time — peak-hour pricing reportedly jumped from roughly $0.87 to $3.96 per million tokens. TheNextWeb’s take was blunt: “DeepSeek built a Claude Code rival, then quadrupled its prices.” Read the two moves together and a strategy comes into focus: give away the tooling layer for free, under a permissive license, and monetize the model layer harder. Anthropic keeps Claude Code proprietary and bundles it with Claude access; DeepSeek is betting that an open harness — one any model can plug into — grows the pie enough to justify charging more for the model that plugs in best.
That’s the business story, and it’s worth knowing before you read the rest of this series. But the business story isn’t why this post exists. I spent the day reading the actual source — not the press releases — and the architecture is the more interesting find. This isn’t a hasty wrapper thrown together for a launch date. It’s a genuinely different, well-considered answer to the same question this blog’s Agent Harness series spent 12 posts answering about Claude Code in April: how do you turn a stateless model into something that can plan, act, recover, and remember?
This series reads dsh as a second worked example — not “the right way,” but a way, built by a different team, with a different foundational bet. Where the two disagree is exactly where the interesting engineering lessons live.
Where dsh sits
Before the deep dive, the fastest way to place a new framework is against the ones you already know:
| DeepSeek Harness | Claude Code | LangGraph | AutoGen | CrewAI | |
|---|---|---|---|---|---|
| License | MIT, open source | Proprietary¹ | Open source | MIT; maintenance mode² | Open source |
| Model lock-in | None — any model via an adapter seam (ships DeepSeek + pi-ai) | Claude models only | None | None | None |
| Core abstraction | Plugins on a shared context; every capability is a swappable “seam” | One hardened harness with hooks and subagents | A graph of nodes with shared state | Agents as participants in a conversation | Role-based agent “crews” |
| Extensibility ceiling | Everything, including the loop itself, replaceable from config | Hooks + custom tools; the loop itself isn’t user-replaceable | Custom nodes and edges | Custom agents and conversation patterns | Custom agents and tasks |
| Maturity today | v0.1 developer preview — expect breaking changes | Production, widely deployed | Most production-mature of the three frameworks | Improving, but deprioritized internally | Solid, active development |
| Best fit | Teams who want to swap the model, sandbox, or persistence layer without forking | Teams who want Anthropic’s polished, batteries-included CLI | Deterministic, auditable, long-running workflows | Multi-agent debate/negotiation patterns | Business-process automation, gentlest learning curve |
¹ Claude Code's source was briefly, unintentionally public: on March 31, 2026, a missing .npmignore entry shipped a 59.8 MB source map — roughly 512,000 lines of unobfuscated TypeScript — inside an npm package. Anthropic called it "a release packaging issue caused by human error, not a security breach" and pulled the package within hours, but not before it was widely mirrored — which is what made April's Agent Harness series possible in the first place.
² AutoGen entered maintenance mode in October 2025; Microsoft's own guidance is that it "will not receive new feature investments but will continue to receive bug fixes, security patches and stability updates," with new work directed to Microsoft Agent Framework (GA April 2026).
The column worth staring at is “extensibility ceiling.” Every framework in that table lets you add a custom agent or a custom tool. Only dsh lets you replace the loop — the thing deciding what happens next — without forking the project. That’s the architectural bet this series is actually about, and it’s worth understanding on its own terms before deciding whether the “rival to Claude Code” framing is fair.
The one-line idea
Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so every part is replaceable from configuration.
—
docs/architecture.md, deepseek-ai/deepseek-harness
Most systems have a core and a plugin layer around the edges — the core does the real work, plugins add optional extras. dsh doesn’t have that split. The thing that decides when to call the model, what the model sees, and how tool calls get dispatched is itself one plugin among roughly 150 others in the repository. Swap it out from a config file and you have a different agent, running on the same runtime, with every other plugin none the wiser.
The architecture doc states the implication directly: “no privileged core to patch.” There’s no core.ts file that everything else defers to. There’s a shared runtime object called a Context, and everything — model adapters, the tool registry, the sandbox, the session log, the loop — is a plugin that mounts services and listens for events on that Context.
Figure: the agent loop is not the trunk everything hangs off — it's a plugin, same as the sandbox or the persistence backend.
This isn’t a novel idea invented for dsh, and DeepSeek doesn’t claim it is. It’s borrowed — deliberately, explicitly — from Cordis, a “meta-framework of spatiotemporal composability” originally built for Koishi, a cross-platform chatbot framework, and formalized in a public design paper, A Programming Paradigm for Spatiotemporal Composability. Cordis’s job is narrow and disciplined: load and unload plugins, resolve their dependencies, and give them a shared context to cooperate through. It doesn’t know anything about LLMs, tools, or agents. dsh vendors a copy of it and builds the entire product as Cordis plugins.
So the genuine novelty here isn’t the primitives — it’s the discipline of applying them to everything, including the one component every other framework in that comparison table treats as fixed: the loop itself.
Four primitives explain almost everything
Once you understand four things about Cordis, most of the design decisions in dsh stop looking arbitrary and start looking inevitable. These aren’t paraphrased from documentation — they’re read straight from the vendored source in the repo.
1. Context: a proxy with a family tree
A Context isn’t a plain object — it’s a JavaScript Proxy, and ctx.extend() creates a child through prototypal inheritance without mutating the parent:
// vendor/cordis/src/context.ts:113-114
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
// vendor/cordis/src/context.ts:127-136 — a separate method, not contiguous with the lines above
extend(meta = {}): this {
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
const self = Object.create(getTraceable(this, this))
for (const prop of Reflect.ownKeys(meta)) {
Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
}
if (!shadow) return self
return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
}
Read ctx.llm from a child and, if the child hasn’t defined its own, the read resolves up the prototype chain to whatever ancestor provided it.
Figure: a plugin sees the services its parents provide; a per-agent scope sees loop services plus its own scoped overrides — for free, from the prototype chain.
This one mechanism is what later posts will build on for scope: a running agent gets its own child context, so it can override a tool or a prompt section for itself alone without touching the global registration.
2. Service: self-registering, statically typed
A Service is a class that registers itself under a string key the moment it’s constructed. Here’s the real constructor, and the real registration it uses in production — ShellExecutor, the abstract base every bash provider extends:
// vendor/cordis/src/service.ts:53-68 (abridged)
constructor(protected ctx: Context, name: string) {
name ??= this.constructor['provide'] as string
// ...
self.ctx = ctx
self.name = name
self.ctx.reflect.provide(name, self, this[symbols.check]) // line 67 — the actual registration
}
// packages/shell/shell/src/index.ts:58-60
constructor(ctx: Context) {
super(ctx, 'shell')
}
That’s it. That’s the entire registration for ctx.shell. TypeScript’s declaration merging then gives every other plugin in the codebase a fully typed ctx.shell, without a central registry file anyone has to edit. A consumer plugin declares static inject = ['shell'], and Cordis holds that plugin back until something has actually provided the service. Activation is service-availability driven, not row-order driven — you can list your rows in any order in a config file and the dependency graph still resolves correctly. Loading a second ShellExecutor throws — Cordis’s standard duplicate-service behavior — which is why sandboxed bash replaces the local provider rather than coexisting with it.
3. Fibers and effects: registration that undoes itself
Every plugin mount is tracked as a Fiber, whose states are declared plainly in the source:
// vendor/cordis/src/fiber.ts:267-274
export const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
DISPOSED,
UNLOADING,
}
Inside a plugin, ctx.effect(body) runs body immediately and collects whatever disposer functions it returns. When the fiber unloads, those disposers run — in reverse order, by design:
// vendor/cordis/src/fiber.ts:466-471 (abridged)
const dispose = () => {
if (disposing) return disposalTask
disposing = true
for (const disposable of disposables.splice(0).reverse()) { // line 471
// ...run each disposer, chaining promises if async
}
}
Figure: a fiber's lifecycle. Because every registration is an effect with a disposer, disposal is never bespoke cleanup code — it's the same mechanism for every plugin.
This is the payoff. Hot module reload, live config patches, and per-agent isolation (an agent running its own private copy of a tool set) are not three separate features requiring three separate implementations. They’re the same operation — dispose a fiber, optionally mount a new one — applied in three different contexts. A framework that makes “undo this plugin’s effects” a first-class, automatic operation gets live reconfiguration almost for free.
4. Events: how plugins talk without knowing about each other
Plugins don’t call each other’s functions directly — they emit and listen for typed events, in one of five dispatch modes (emit, parallel, serial, bail, and the important one, waterfall, where listeners wrap a next() continuation and can intercept, rewrite, or refuse to delegate). This is the mechanism behind policy decisions like “what does the model see this step” or “should this request retry,” and it’s substantial enough to earn its own post later in this series — Part 4 covers it in full. For now, the thing worth knowing is that it exists as a fourth primitive alongside Context, Service, and Fiber, and that dsh leans on it everywhere rather than inventing bespoke hook systems per feature.
What “no privileged core” buys you, concretely
Here’s the payoff stated as a mental model rather than a slogan. dsh’s own docs describe the product as six layers, and the load-bearing property is the direction of the arrows: the loop only ever talks downward to a capability’s abstract interface — never to a specific provider.
Figure: the loop (blue) talks to seams, never to concrete providers below them. Swap what's behind a seam and everything above it is unaffected.
Take the sandbox as the concrete example, since it’s the one the docs use to make this vivid. ctx.shell is an abstract interface. LocalBashExecutor provides it by spawning a subprocess directly. SandboxBashExecutor provides the exact same interface, but wraps the command through bwrap or Landlock or Seatbelt before spawning it. The tool that calls bash — dsh-tool-bash — depends only on ctx.shell. It has never heard of sandboxing. Change one row in a YAML config file from the local provider to the sandboxed one, and every consumer of ctx.shell — the bash tool, the PTY-backed terminal, the LSP client that shells out to language servers — gets confined, with zero code changes anywhere else in the stack.
That’s the concrete version of “no privileged core.” There’s no if (sandboxed) branch buried in the tool’s implementation. The tool doesn’t know sandboxing exists. The decision lives entirely in which plugin got mounted. The same reasoning is why the agent loop is real, ordinary application code rather than framework magic — ReactLoopAgent, the default driver, is a class like any other, with its turn() method sitting right there in the open at packages/core/agent-loop/src/agent.ts:266. Nothing about it is privileged; it’s simply the plugin that happens to be mounted at the loop seam today.
Try it yourself
Before we go further: this is a live, running system, not a design doc. The README’s quickstart is one command:
npx @deepseek-ai/dsh web
That boots the web UI at http://127.0.0.1:3080. Everything in this series is inspectable in a running process, not just readable in source — a fact that matters more than it sounds, because the next post is entirely about the mechanism that assembles that running process from nothing but YAML.
The three invariants worth remembering
Everything in the rest of this series traces back to three rules that fall directly out of the four primitives above:
The second one is the boldest claim of the three, and it’s worth sitting with before we move on: anything the model can see must be reconstructable from an append-only log. Not “should be logged for debugging” — the codebase asserts this as a runtime invariant and fails loudly if a request to the model doesn’t byte-equal what the log would reconstruct. That single rule is what makes replay, forking a session mid-conversation, and crash recovery all work the same way, for every feature, without each one inventing its own persistence story. It’s the subject of Part 3, and it’s the idea in this whole series I’d bet you’ll still be thinking about a week later.
Coming up in this series
Given how fast this news is moving, I’m publishing daily rather than spacing this out over two weeks — the goal is to finish the series while dsh is still the thing people are actually evaluating. (Dates below are the plan, not a promise — this is a v0.1 preview repo and each post digs into real source, so if a day slips the series will still land, just a day or two later than listed.)
| Part | Date | Title | What it covers |
|---|---|---|---|
| 1 | Aug 13 | DeepSeek Harness: Inside the Open-Source Claude Code Rival (this post) | The launch, the comparison, no privileged core, the four Cordis primitives |
| 2 | Aug 14 | Composing an App From YAML, Not Code | Profiles, bundles, patch layers, boot, live HMR |
| 3 | Aug 15 | The Session Log Is the Only Source of Truth | Event sourcing, the surface projection, model-visible ⟺ logged |
| 4 | Aug 16 | Waterfalls: One Pattern for Every Policy Decision | The event system, durable vs. live events, retry |
| 5 | Aug 17 | Capability Seams: Making Bash Swappable for Sandboxed Bash | The 3-role pattern end-to-end; scope and shadowing |
| 6 | Aug 18 | Turns, Steps, and Cancellation That Doesn’t Corrupt the Log | The agent loop, the inbox, clean cancellation |
| 7 | Aug 19 | What a Second Production Harness Teaches | dsh vs. the Agent Harness series, side by side |
If you read the Agent Harness series back in April, keep its vocabulary in mind as we go — permission pipeline, memory system, hook system, streaming architecture. Every one of those problems reappears here. The interesting question, which Part 7 answers directly, is how many of them dsh solves with the same primitive instead of a bespoke one.
References
- DeepSeek AI,
deepseek-ai/deepseek-harness— the repository (MIT license, developer preview, released August 13, 2026) docs/architecture.md, deepseek-ai/deepseek-harness- Source citations pulled directly from the repo as of commit
47f9438(master, Aug 13, 2026 — this is a fast-moving v0.1 preview, so line numbers may drift in later commits):context.ts,service.ts,fiber.ts,agent.ts,shell/index.ts cordiverse/cordis— “Meta-Framework of Spatiotemporal Composability,” the plugin runtime dsh vendorscordiverse/paper— “A Programming Paradigm for Spatiotemporal Composability,” the design paper behind Cordiskoishijs/koishi— the chatbot framework Cordis was originally built for- VentureBeat, “DeepSeek Harness launches as open source rival to Claude Code, alongside V4-Pro on API with higher prices”
- TheNextWeb, “DeepSeek built a Claude Code rival, then quadrupled its prices”
- Decrypt, “DeepSeek Is Building Its Own Claude Code. Beijing Wants the Whole Stack.”
- The New Stack, “DeepSeek open sources an agent harness where everything is a plugin” (Frederic Lardinois, Aug 13, 2026)
- Layer5, “The Claude Code Source Leak: 512,000 Lines, a Missing .npmignore, and the Fastest-Growing Repo in GitHub History” — source for the Claude Code leak referenced in the comparison table
- VentureBeat, “Microsoft retires AutoGen and debuts Agent Framework to unify and govern enterprise AI agents” — source for AutoGen’s maintenance-mode status referenced in the comparison table
- This blog’s own Agent Harness series, for the comparison this series is written against
Next: Part 2 — how a running dsh process is assembled, entirely from YAML, before a single plugin’s apply() function runs.
Have thoughts on this?
I read every email. If something resonated, felt wrong, or made you think — I'd love to hear from you.
Comments