Our DeepSeek Harness plugin turned out to be nine lines of YAML
Before writing an agent-harness plugin, check for an MCP config slot. Ours was nine lines of YAML, and the host strips credential env vars from stdio children.
A new agent harness comes out and your system needs to show up inside it. For you, that might be memory, a tool server or a policy layer. Reading “everything is a plugin” in the README, most of us start writing a plugin: a package, a repo, a hook on the agent loop. I did exactly that on paper. Then I re-read the harness’s repo before writing any code, and the plugin went away. What was left was a config block, plus one trap in how the harness spawns child processes that would have failed without a sound.
@deepseek-ai/dsh-mcp-client already had an empty memory slot
DeepSeek Harness (dsh) is an MIT-licensed, model-agnostic coding-agent loop built on Cordis. Its architecture doc is blunt about it: the model adapter, the tool registry, the session log and the agent loop are all plugins. None of them is privileged, and you extend the harness “by mounting a plugin beside the others.” A running dsh is a plugin tree put together at boot from layers: a profile, then bundle patches, then a home patch, then any --patch overlay.
One of the plugins it ships is an MCP client. The repo documents three memory systems attached through it as plain configuration. There was no slot for ours. So the capability I shipped is an install command that writes one entry into the machine-wide home patch, $DSH_HOME/cordis.patch.yml. Every profile on the machine picks it up:
- insert:
- id: memory-vodou
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: vodou
transport: stdio
command: /path/to/vodou-core
args: [mcp-server, --profile, memory, --client-id, dsh]
cwd: /path/to/project
The part I had not expected: dsh-mcp-client registers the server’s tools through ctx.tools, which is the harness’s own tool registry. The memory tools don’t just reach the CLI. They reach any app built on the framework.
The plan was an npm package, a GitHub repo and a dsh-plugin topic tag
The phase as drafted had three deliverables. First, an npm package called @vodou/dsh-plugin-memory. Second, a GitHub repo tagged with the dsh-plugin topic so people could find it. Third, Cordis plugin code hooked on agent/pre-step to inject memory before each step. The estimate was a day and a half.
I had been burned by building against a fast-moving repo from memory, so I made a re-read a blocking gate with six questions. Does a plugin API still exist? Do agent/pre-step and agent/turn-stopping still exist under those names? What is the manifest shape? Is dsh-plugin still the discovery topic? May a plugin reach a local MCP server? What does a hook actually receive?
The hooks were real, with 168 and 44 usages in the tree, and the topic was real too. None of that ended up mattering, because the answer to question five was “you don’t need a plugin for that.” The package disappeared, and so did the repo and the tag. That includes the specific failure the gate was written to prevent: publishing under a discovery tag nobody reads. A day and a half turned into half a day.
dsh’s stdio bridge deletes credential-shaped variables before it spawns you
The re-read also turned up the trap. The harness’s stdio bridge “deliberately removes ambient variables whose names usually identify credentials and all DSH_* variables before launching a child.”
That’s a sensible security default. It’s also invisible to anyone who designed against a host that passes its environment through. Our MCP egress identifies each client, and a design that read a token or client id from an inherited variable would have spawned fine. It would have listed tools and then either failed auth or been recorded under the wrong client. Nothing would have errored at install time.
It couldn’t bite us, but only by luck of an earlier decision: our MCP server defaults to stdio and takes every setting on argv. The --client-id dsh in the YAML above isn’t decoration. It’s the one identity that appears both in the spawn line and in the audit rows written on the other side of the connection.
Four clients agreed on “a map of servers.” The fifth was a YAML patch list
Before this, the installer knew four clients. Every one of them used a JSON object with a map of server entries, and they disagreed only on the key name (mcpServers versus servers). So the installer modelled “a client” as a file path plus a key name, and that worked until dsh.
dsh isn’t a map with a different key. It’s a YAML sequence of patches, and our server arrives as a plugin instance with its own id, not as a value keyed by server name. I could have faked it: pretend the list is a map, look entries up by position, special-case the write. Every later fix would then have had to work around that lie. Instead, the client description gained an explicit config format, and the YAML writer is its own code path. It treats a missing or empty patch file as an empty list rather than an error, since the file doesn’t exist until someone patches something. It also replaces our entry by id wherever it sits, so a reinstall updates in place instead of appending a duplicate. The command and args still come from the same builder the four JSON clients use, so the profile and client-id rules can’t drift between hosts.
A smaller one: $DSH_HOME is read from the environment and falls back to ~/.dsh, the same way the harness resolves it. The process environment wins.
An MCP host graded from chat rows showed nothing but dashes
We keep a registry of host adapters and grade each one from real evidence of use. The existing hosts all create conversations, so their evidence lives in the chat database. An MCP client never creates a conversation. Its evidence is an egress audit row keyed by client id. When MCP-attached hosts were graded as if they were chat hosts, every one of them read as all dashes. So MCP became its own transport in the registry, graded from the table that transport actually writes. The public registry entry is small:
[hosts.dsh]
transport = "mcp"
surface = "dsh"
pipeline = "mcp-egress"
verbs = ["recall"]
status = "stable"
Two invariants you can grep for
Stated as properties that are either true or false of a codebase:
An integration writer is typed by the host’s config grammar, not configured by a key name inside a grammar you assumed. If your installer has one struct with a servers_key string and a single merge routine for every host, it fails this. It will break, or get quietly special-cased, on the first host whose config is a list, a patch or an instance.
Nothing a stdio tool server needs to authenticate or identify its caller comes from inherited environment. It arrives on argv, or in a file that argv names. If any code path reads a token or client id from process.env or os.environ in the spawned process, it fails this under any host that filters the environment.
Spawn a probe server inside your host and read what arrived
This takes five minutes and uses nothing of ours. Save it as probe.py:
#!/usr/bin/env python3
import json, os, sys, time
with open("/tmp/mcp-spawn-probe.json", "w") as f:
json.dump({"argv": sys.argv, "cwd": os.getcwd(),
"env": sorted(k for k in os.environ if k.startswith("PROBE_"))}, f, indent=2)
time.sleep(5) # stay alive long enough to look like a slow server, not a crash
Register it as a stdio MCP server in whatever host you target, with command: python3 and args: [/abs/path/probe.py, --client-id, probe]. Then launch the host from a shell where you’ve set PROBE_API_KEY=x PROBE_TOKEN=y PROBE_PLAIN=z. The host will complain that the handshake failed. Ignore that and read the file:
jq . /tmp/mcp-spawn-probe.json
If the host passes everything through, env lists all three PROBE_ names. A filtering host shows something like ["PROBE_PLAIN"], or an empty list. Now check whether your own server depends on what went missing:
grep -rnE "process\.env\.|os\.environ|getenv\(" src/ | grep -iE "key|token|secret|auth|client"
You fail if that grep returns a line your server needs at startup and the probe shows the host strips it. argv should contain --client-id probe exactly as written. If it doesn’t, the host is rewriting your spawn line too.
Then check that your installer is idempotent and leaves neighbours alone:
cp "$HOST_CONFIG" /tmp/before
your-installer install host && your-installer install host && your-installer uninstall host
diff /tmp/before "$HOST_CONFIG" && echo PASS
A pass prints PASS. A fail is a diff: a duplicated entry, a neighbour’s entry reordered or dropped, or reformatted quoting. When I ran this round trip against a temp home, a neighbouring memory entry survived untouched.
Verification guides start after the tool is already wired
The agent-verification writing I read is good at the step between “the agent says done” and “the write lands.” self.md’s loop runs seven checks before any write, send or deploy. Anna Jey’s guide asks whether the agent “called the right tools.” Neural pruning adds a monthly audit for stale context and unused skills.
All three assume the tool the agent calls is the tool you think it is, spawned with the identity you think it has. A stripped credential doesn’t make a tool disappear. The tool still lists, still gets called and still returns something. The agent passes every one of those checks while it’s talking to an unauthenticated server. The seam between a harness and a tool server needs its own check, and it has to run where the harness actually spawns things, not in a unit test that builds the config in memory. The dsh config catalog is where that kind of detail lives. It’s a deployment reference, not the plugin-author docs, and it’s the page you skip when you’ve already decided to write a plugin.
Verified against a client that spawns like dsh, not against dsh
To test this, I wrote a real stdio MCP client (curl proves nothing about a protocol server). It spawned our server with exactly the argv above. The handshake completed, it listed four tools, and a memory search returned a stored fact. Install, status, reinstall and uninstall all round-tripped against a temp DSH_HOME, and six tests pin the YAML shape against dsh’s own example file.
That proves our side end to end. It does not prove dsh itself, which I didn’t have installed for that run. The protocol and the spawn shape are the same, but “the same” is a claim I haven’t watched happen inside the real harness. Two more things are still open. The grading half of the host registry wasn’t in this change, so dsh currently grades as no-evidence even when it works. And the writer parses and re-serializes the patch file, so any comment someone hand-wrote in cordis.patch.yml doesn’t survive an install.