SherlockLiu Logo SherlockLiu
Back to all posts
Engineering

DeepSeek Harness: Inside the Open-Source Claude Code Rival (Part 1)

SL
Aug 14, 2026 13 min read
DeepSeek Harness: Inside the Open-Source Claude Code Rival (Part 1)

Series: Inside the DeepSeek Harness — Part 1 of 16


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.

A note on what this series is, and isn’t. By the time this went to print, plenty of good writing already existed on using dsh — install steps, demo tasks, a tour of its runtime modes. This series is not that. Every post here reads the actual source (pinned to a single commit, cited by file and line) and asks a narrower, harder question: what’s the design pattern, and where has this exact problem been solved before — usually by Claude Code, four months earlier, with a different answer? If you want a tutorial, the rest of the internet has you covered. If you want to know why an “agent mode” is a directory instead of a class, or why a guard can only return deny, never allow, keep reading.


A week in: the numbers, and the gap the tutorials left

dsh didn’t just launch fast — it’s still accelerating. The repository crossed 80,000 GitHub stars in under 24 hours, a faster first-day climb than Grok-1’s run to 20k (1.2 days) or DeepSeek-R1’s (5.7 days). And it isn’t only stars: within that same first day, the dsh-plugin GitHub topic tag already listed 300+ community plugins — skins, a file-attachment helper, and modlens, the first plugin that gives the agent eyes on a screenshot. An official “Awesome” list appeared to curate them.

That plugin explosion is the loudest signal in this whole story, and almost none of the early coverage asked why it was possible this fast. The answer is a design decision this series didn’t cover in its first 15 posts, because it shipped after they were drafted: Agent Presets, dsh’s four runtime modes (Standard, Code/PTC, Minimal, Creator), each one a directory on disk rather than a hardcoded class — plus Code Mode / PTC, a genuinely different answer to “how does the model orchestrate five tool calls at once” than anything in Claude Code’s playbook, with a real, reported tradeoff (it makes weaker models write broken orchestration scripts). Part 16, added to this series once that gap was clear, covers both in full — including the sharpest Claude Code comparison in the entire series.


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 mounted in a typical profile (the workspace itself has over 200 packages, not all of which are plugins — some are generators, test support, or shared libraries). Swap the loop plugin out from a config file and you have a different agent, running on the same runtime, with every other plugin none the wiser.

That’s not a loose estimate, either — the repo generates and CI-verifies a full capability graph, and it currently lists ~85 named ctx.* services. Almost every one of those is a seam we’ll spend the rest of this series pulling apart: ctx.shell, ctx.fs, ctx.sandbox, ctx.llm, ctx.subagents, ctx.workflowEngine, ctx.jobs, ctx.terminals, ctx.goals — each independently swappable, each following the same three-role pattern this series builds toward in Part 5.

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.

Context one shared runtime object dsh-agent-loop dsh-llm dsh-session dsh-tool-bash dsh-sandbox dsh-session-persistence-jsonl six of ~150 plugins in the repository — all mounted the same way

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:74-75
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
// vendor/cordis/src/context.ts:99-107 — 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.

ctx (root) loader tree ctx.extend() entry ctx: dsh-llm row agent.ctx — a per-agent scope reads walk up the chain

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:147-154
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:427-431 (abridged)
const dispose = () => {
  if (disposing) return disposalTask
  disposing = true
  for (const disposable of disposables.splice(0).reverse()) {  // line 431
    // ...run each disposer, chaining promises if async
  }
}
PENDING LOADING ACTIVE DISPOSED unload → disposers run in reverse order → every contribution vanishes cleanly

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.

Surfaces — CLI, web app, ACP server, SDK clients Composition — profiles, bundles, patch layers (boot from YAML) The plugin spine — agent-loop, session, tools registry Capability seams — shell, fs, sandbox, lsp, web, subagent... Providers — local, sandboxed (bwrap/Landlock/Seatbelt/ACL), e2b Models — DeepSeek adapter, pi-ai adapter, retry policy

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:246. 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:

1. Registrations are effects ctx.effect(body) runs now, disposers unwind on unload → Part 2: composition and HMR 2. Model-visible ⟺ logged anything the model sees must be reconstructable from the session log → Part 6: the session log 3. A seam has three roles Service Definition → Provider → Consumer, always all three → Part 5: capability seams

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

This series runs all 16 parts today — the whole thing at once, rather than trickled out over two weeks, because the DeepSeek Harness news cycle moves faster than a daily posting schedule does. It grew from a planned 7 parts to 16 once the research turned up how much real depth (and how many real incidents) the codebase actually has to show.

Part Title What it covers
1 DeepSeek Harness: Inside the Open-Source Claude Code Rival (this post) The launch, the comparison, no privileged core, the four Cordis primitives
2 Composing an App From YAML, Not Code Profiles, bundles, five patch layers, boot, live HMR, --dump-config
3 Scope: Why a Live Agent Is the Key of Its Own Registration Shadowing, restriction, lineage vs. scope
4 Tool Execution in DeepSeek Harness: Guards and Approval Pre-execute, monotonic guards, post-execute, approval
16 DeepSeek Harness: Agent Presets as Data, Not Code Agent presets, PTC/Code Mode, the plugin ecosystem, compared to Claude Code’s agent types
5 Capability Seams: Making Bash Swappable for Sandboxed Bash The 3-role pattern end-to-end, across ~85 real seams
6 The Session Log: DeepSeek Harness’s Enforced Invariant The real invariant-checking code, surface projection
7 Persistence and Compaction: Crash-Safe by Construction JSONL/SQLite, torn-tail repair, the compaction lock bracket
8 Waterfalls: The One Event Pattern That Runs Everything Five dispatch modes, durable vs. live events, retry
9 The Agent Loop: Turns, Steps, and a Real Cancellation Bug The phase state machine, the inbox, a dated bug fix
10 The LLM Layer: One Message Format, Every Surface Message vocabulary, streaming, retry-via-log-replay
11 Subagents and Workflows: Composing Agents From Agents Provider kinds, continuable children, Ralph rounds
12 Defense in Depth: Sandboxing and Four Real Incidents bwrap/Landlock/Seatbelt/ACL, real production postmortems
13 Three Surfaces, One Spine: Web, Typert RPC, and SDK/ACP Typert codegen, the four-quadrant RPC envelope
14 Engineering Rigor: DeepSeek Harness’s Verification Gate The 100% coverage gate, real engineering-culture quotes
15 What a Second Production Harness Teaches dsh vs. the Agent Harness series, side by side

Part 16 shipped after this series’ initial 15 posts, once Agent Presets and Code Mode landed — it reads best right after Part 4, which is why its row sits there instead of at the end.

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

Next: Part 2 — how a running dsh process is assembled, entirely from YAML, before a single plugin’s apply() function runs.

Comments