Two weeks with DeepSeek Harness: five plugins that bend an AI client to your own toolchain
A while ago I wanted an AI client that lets me add my own tools. I tried a few, and they all hit the same wall: the available tools are fixed by the platform; memory lives in the platform's database and can't be exported; and there's no entry point for plugging in my team's own scripts.
Then I installed DeepSeek Harness (DSH for short). I didn't expect much at first, until I opened its installation directory, saw 190+ packages, and clicked through layer by layer. The only package actually running the agent loop is dsh-agent-loop; everything else is replaceable parts. Its README has one line: write new behaviors as plugins, don't modify the kernel.
That one line got me to spend two weeks carefully studying it and writing 5 plugins, all open-sourced on npm. This post first talks about these 5 plugins, then explains DSH's structure and its boundaries.
The 5 plugins I wrote
- dsh-memory-git: stores long-term memory as Markdown, kept in the user's own Git repository. 5 tools: write, search, read, sync, self-check; 78 tests.
- dsh-git: a Git panel in the right sidebar — branches, commit graph, staging, diff, push/pull. 20 tests.
- dsh-ai-chat-mcp: turns DSH into an MCP server; Claude Desktop, Cursor, n8n can all call its session capability. 12 tools.
- dsh-automation: macOS desktop and browser automation, 6 tools, from screenshot/recording to mouse/keyboard and CDP browser.
- dsh-computer-use: Windows version, feature-aligned, with the underlying layer switched to a resident PowerShell worker process and UI Automation.
All five plugins are on GitHub under the MIT license. No config changes needed before installing; just enter the package name in DSH's settings.
The most memorable part was memory-git's sync. A pull --rebase conflict leaves things in a half-applied state, and the user's next write would fail mysteriously. It took a long time to figure out, and I changed it to automatically roll back on failure. The automation plugin also cost me a lot of time: I kept testing screenshots in bash and kept getting permission errors, until I realized bash itself is in a sandbox, while the plugin runs in the Host process, where permissions are actually fine.
How these plugins plug in
The 5 plugins above use different extension points. While writing them I explored DSH's structure, and this part explains why it's extensible this way.
DSH's shell is a desktop app, but the logic actually runs in its internal plugin system, built on Cordis. Cordis is a TypeScript plugin framework that provides dependency injection, scoped services, and lifecycle management. Its core loop is just one package; everything else is a plugin hanging off extension points.
This means writing plugins mostly doesn't touch the kernel. My 5 plugins are independent npm packages that only depend on Node built-in modules. After publishing, users install and they work.
Digging further, it heavily uses the same pattern — officially called service splitting, which I call three-layer abstraction:
- Definition: defines the capability contract, e.g., sandbox's
confine(argv, policy) - Provider: the actual implementation; macOS uses Seatbelt, Linux uses bwrap or Landlock
- Consumer: the caller, e.g., the bash tool
This split makes a big difference in practice. For example, the sandbox — today it's sandbox-exec; swapping to another implementation won't require changing the bash tool. This structure is everywhere inside, which is why it dares to say "write new behaviors into plugins."
The extension points available when writing plugins, as I count them, fall into four categories.
The first is Host plugins, which add tools to the model. This is the most common kind. The plugin runs in the Host process and registers tools via ctx.tools.register. Parameters are native JSON Schema, the return value rendering is up to you; there's no SDK and no compilation step. memory-git, ai-chat-mcp, and the two automation plugins are mostly this kind. My first hello world was only a dozen lines.
export const inject = ['tools']
export function apply(ctx) {
ctx.effect(() => ctx.tools.register({
name: 'hello',
description: 'Say hello',
parameters: { type: 'object', properties: {}, additionalProperties: false },
output: {
schema: { type: 'string' },
render: (value) => [{ type: 'text', text: value }],
},
execute: async () => 'hello from my plugin',
}))
}
The second is Client plugins, which add things to the UI. Here it's injection, not replacement: the client pre-declares a set of slots, and plugins can only insert components into those slots. dsh-git's panel sits in the right sidebar's tabs and the conversation title bar. This means you can add what you need inside the existing UI, but you can't change its shape.
The third is Skills. Skills are operation manuals for the model; they're not all stuffed into context at once. Normally only a one-line summary is shown, and the full text is loaded only when used. This design matters, otherwise with many plugins, just tool descriptions would fill up the context.
The fourth is MCP. DSH itself can act as an MCP client, connecting to external MCP services; tool names become mcp__server_name__tool_name. Conversely, it can act as an MCP server, which is what dsh-ai-chat-mcp does.
There are also three things that aren't extension points but determine whether plugins can run safely: sandbox, approval, and sub-agents. The sandbox has three levels: read-only, workspace-write, danger-full-access. Each call passes a policy; if the backend is unavailable, it errors out directly rather than letting it through. Approval means sensitive operations ask a human first. Sub-agents split large tasks into sub-sessions with independent contexts. I had to consider all three when writing plugins, especially for file writes and network requests.
Its boundaries
After two weeks, I also hit its boundaries. The capability surface can indeed expand indefinitely, but other layers can't.
The capability surface has almost no ceiling because plugins are Node code with full permissions — they can start servers, run native commands, compile native helpers, call any API. The context surface can also expand: Skills load on demand, MCP hooks external services, sub-agents isolate — all of these ease the "too much stuff to fit" problem.
But the interaction surface is limited. You can only inject into officially declared slots; the core layout, input box, and streaming rendering protocol can't be changed. The distribution surface also doesn't hold: plugins are appendages of the host; turning one into a standalone product means building your own shell, signing, and app store listing. Real-time pipelines aren't supported either; interaction is turn-based request/response.
There are also three hard engineering boundaries.
One is trust. Host plugins run in a non-sandboxed process, so capability has no ceiling, meaning trust has no ceiling either. Installing a third-party plugin is essentially running someone else's code. Before installing a plugin now, I check what tools it registers and whether it sends requests out.
Two is tool overload. Tool descriptions take up context; the more tools, the higher the chance the model picks wrong. That's why Skills need on-demand loading.
Three is single-process coupling. A plugin crash or hang drags down the host. Also, native capabilities are limited by system permissions, e.g., macOS accessibility and screen recording.
My own feeling is that DSH is more like VS Code than Chromium. It can grow all kinds of tools within its own shape, but it can't grow products outside that shape.
About "self-evolution"
I didn't believe this term at first — it felt mystical. After two weeks, my understanding is: it doesn't mean the model changes itself; it means capabilities can settle down.
I broke it into three layers.
The first is tool sedimentation. An operation written in code today can become a plugin tomorrow. My 5 plugins are 5 instances of sedimentation; on a new machine or new project, install and use.
The second is experience sedimentation. dsh-memory-git lets preferences and pitfalls persist across sessions. The naming conventions, release process, and how tests run from this plugin-writing session can be reused next time directly.
The third is process sedimentation. Skills can store operation manuals, sub-agents can split big tasks, workflows can chain multiple steps. Processes themselves can be solidified.
During this process I did heavily use DSH to write DSH plugins: having it spawn sub-agents to divide up module writing, run tests, fix CI, and finally I review and release. But I don't want to make it sound magical. In practice, AI-written code still needs to be reviewed line by line, tests still fail, especially around system permissions and boundaries — those need my own verification. Its value lies in compressing repetitive labor, not replacing your judgment.
One more thing I care about: these capabilities ultimately all land in files — plugins in my repository, memory in my Git, skills as Markdown. Even if I stop using DSH someday, these things remain. That matters more than the word "self-evolution" itself.
If you want to write one yourself
Writing the first plugin only takes three steps.
Declare bundle and version admission in package.json:
{
"name": "@you/dsh-hello",
"version": "0.1.0",
"type": "module",
"exports": { ".": "./index.js" },
"files": ["index.js", "cordis.patch.yml", "README.md"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
"engines": { "node": ">=20", "dsh": ">=0.2.1-rc.3 <1.0.0" }
}
engines.dsh is version admission. If it doesn't match, the plugin page will show an incompatibility warning instead of blowing up after installation.
Then write cordis.patch.yml:
- insert:
- id: hello
name: '@you/dsh-hello'
Finally index.js, which is the ctx.tools.register snippet above.
After writing, npm publish. Users enter the package name on the plugin page to install. Two pitfalls when publishing: if npm has 2FA enabled, publishing needs a bypass-2FA token; also, the default registry in China is npmmirror, which is a read-only mirror, so publishing requires explicitly specifying the official source.
Finally
After two weeks, I think DSH currently suits two kinds of people: developers who want AI to plug into their own toolchain, and people willing to spend time sedimenting repetitive work into tools. If you just want out-of-the-box, no config fiddling, it may not be the most convenient choice.
It's still in rc stage, plugin count is low, but the upside is that plugins written now are easier to get noticed.
The repositories for all 5 plugins are here; star or issues welcome:
- https://github.com/auraflame-ywt/dsh-memory-git
- https://github.com/auraflame-ywt/dsh-git
- https://github.com/auraflame-ywt/dsh-ai-chat-mcp
- https://github.com/auraflame-ywt/dsh-automation
- https://github.com/auraflame-ywt/dsh-computer-use
Official docs: https://deepseek-harness.github.io/deepseek-harness/
If you're also writing DSH plugins, feel free to reach out~