The thing DeepSeek open sourced on 13 August is not a model. DeepSeek Harness, or dsh once it is on your PATH, is the runtime that wraps a model and turns it into an agent: the turn loop, the tool calls, the kernel sandbox those tools execute inside, the session log, and a browser UI to drive the whole thing. The model itself is a plugin. So is the sandbox. So is the web server.
That distinction is where most people lose the first five minutes. Installing DeepSeek Harness does not get you a better model, it gets you a harness you can rewire: point it at DeepSeek, at Anthropic, at your own Ollama box, and keep every session on your own disk. This guide covers what dsh actually is, how a single task moves through it, the Node version floor that bites on older Ubuntu, and a first agent run that edits a real file and proves the fix. The commands, captured output and timings below were run on a clean Ubuntu 26.04 VM against dsh 0.1.0-rc.7 in August 2026; the Ubuntu 24.04 figures come from the archive.
What DeepSeek Harness actually is
DeepSeek Harness is an MIT-licensed agent harness: the code between a language model and your filesystem. It ships the pieces a coding agent needs and leaves each one swappable, because the whole thing is built on the Cordis plugin framework and the organising rule is that everything is a plugin. The model route is a plugin row. The bash tool is a plugin row. The permission policy that decides whether that bash tool may write to /etc is a plugin row.
The repository went public on 13 August 2026 alongside the V4-Pro launch and passed 167,000 stars inside a week, which is the fastest ramp of anything we have covered here. It is still labelled a developer preview, and the project says so in capital letters: expect compatibility-breaking changes between release candidates. Pin the version you tested.

What you get in the box, on top of that plugin tree: a Web UI, a one-shot headless mode, a Python SDK that bundles its own Node runtime, an MCP bridge, an Agent Client Protocol server, an LSP tool, a PTY terminal, subagents, workflows, plan and goal state, context compaction, and a hooks bridge that maps the supported subset of an existing Claude Code hooks.json onto the harness hook surface once you point it at the file. If you already run agents, the useful mental model is that dsh occupies the same slot as Claude Code or OpenCode, except the seams are exposed and the model behind it is your choice.
How a task moves through dsh
Nothing the model asks for runs directly. Every turn follows the same path, and knowing that path is what makes the permission modes and the session log make sense later.

Your prompt goes to the runtime, the runtime assembles the system prompt plus the tool list plus the session so far and sends one request to whichever model route you configured. The model replies with a tool call, not with an action. The runtime is what turns that request into a real process, wraps the argv in a sandbox, runs it, and hands back stdout, stderr and the exit code. Every one of those steps is appended to the session log before the answer reaches your screen.
The run stats are visible at the bottom of the UI, and they are worth reading once. Fixing a one-line bug in a 15-line Python file cost this much:
1 turns . 4 steps | LLM 7.3s . Tool call 0.1s | TTFT avg 1.1s . 190 tok/s | Cache hit 75% | Input 32.6K tok . Output 572 tok
32,600 input tokens for a task that needed 572 output tokens. That is the standing cost of the system prompt, the tool schemas and the runtime context injection, and it is the same shape of overhead we measured when digging into context engineering for AI agents. Prompt caching absorbed 75% of it here, which is why the turn still finished in seven and a half seconds.
The session itself lands under $DSH_HOME/sessions/, one directory per workspace, as an append-only JSONL stream compressed with zstd. That one task produced 81 events. Reading it back needs the zstd tools, which are not on every minimal image:
sudo apt install -y zstd
zstdcat ~/.dsh/sessions/--home-jmutai-demo-app--/session-e6f0e952-*/session.jsonl.zstd | head -4
Name the session you want rather than globbing the workspace, because a workspace accumulates one directory per run. The first four lines tell you the permission preset, the sandbox mode and the approval policy the session was born with:
{"type":"session","version":0,"id":"session-e6f0e952-5352-4756-a445-37d5f40a74ff","createdAt":1787176266678,"cwd":"/home/jmutai/demo-app","delegationDepth":0,"agentPreset":"standard"}
{"type":"permission/preset","seq":0,"time":1787176266868,"data":{"preset":"workspace-write"}}
{"type":"sandbox/mode","seq":1,"time":1787176266872,"data":{"mode":"workspace-write"}}
{"type":"approval/policy","seq":2,"time":1787176266873,"data":{"policy":"ask"}}
Resume, fork, search and replay all read that same stream, so there is no second database to keep in sync.
What dsh needs before you install it
One hard requirement and one soft one. The hard requirement is Node: the repository declares ^22.19.0 || >=24.0.0. That range lives in the private root manifest and never reaches npm, so npm install prints no engine warning and installs happily on an unsupported runtime. The failure arrives later, at first launch. The soft requirement is bubblewrap, which the sandbox prefers on Linux and falls back from when it is missing.
| Item | Ubuntu 26.04 (Resolute) | Ubuntu 24.04 (Noble) |
|---|---|---|
| Archive Node version | 22.22.1, meets the floor | 18.19.1, too old |
| Install path | apt install nodejs npm | NodeSource or nvm for Node 22/24 |
| Landlock in kernel | Yes (kernel 7.0, CONFIG_SECURITY_LANDLOCK=y) | Yes (kernel 6.8) |
| bubblewrap | apt install bubblewrap, 0.11.1 | apt install bubblewrap, 0.9.0 |
On 26.04 the archive Node is new enough, which is a rare and pleasant thing. Anything older needs a real Node install first.
SyntaxError: The requested module ‘node:util’ does not provide an export named ‘parseEnv’
This is what Node 18 and older produce, and it is worth recognising because the message never mentions Node versions and the install itself succeeded without a warning. Running the exact same dsh install under Node 18 gives:
file:///usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-app-boot/lib/index.js:4
import { parseEnv } from "node:util";
^^^^^^^^
SyntaxError: The requested module 'node:util' does not provide an export named 'parseEnv'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
Node.js v18.20.8
util.parseEnv landed in Node 20.12 and 22, so the import fails at module instantiation before any dsh code runs. Check node -v first and the whole class of problem disappears.
Install Node.js and the dsh package
On Ubuntu 26.04 both packages come straight from the archive:
sudo apt update
sudo apt install -y nodejs npm
Confirm the runtime clears the floor before going any further:
node -v
Anything starting with v22.19 or newer is fine:
v22.22.1
On Ubuntu 24.04 the archive gives you Node 18, so pull a supported line from NodeSource instead of fighting it:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Now the harness itself. A global install puts dsh on PATH for every shell and every systemd unit later:
sudo npm install -g @deepseek-ai/dsh
It is a big dependency tree. On a 4 vCPU VM the install pulled 455 packages in about a minute:
added 455 packages in 1m
Check what you actually got, because the npm latest tag and the repository head are not the same release candidate:
dsh --version
At the time of writing latest resolves to rc.7 while next carries rc.8:
0.1.0-rc.7
If you would rather not install anything, npx @deepseek-ai/dsh web works and downloads the same tree into the npx cache on first run. The gotcha there is that the first invocation looks frozen for a minute or two while npm fetches 455 packages with no progress output. Install globally on any box you will use more than once.
Whichever route you take, the subcommand is not optional. The launcher exits with error: --profile <name> is required when you run dsh on its own.

That is the whole install. The binary is on PATH and the server answers on loopback, which is everything the next step needs.
Start the Web UI and give it a model
Start the server from the directory you intend to work in, because the invoking directory becomes the default workspace root:
cd ~/demo-app
dsh web --no-open
It prints one line and stays in the foreground:
dsh web: http://127.0.0.1:3080
Drop --no-open on a desktop and it opens your browser for you. That bind address is not decoration, and it is worth verifying:
ss -ltnp | grep 3080
Loopback only, which is the correct default for a process holding an API key and holding write access to your source tree:
LISTEN 0 511 127.0.0.1:3080 0.0.0.0:* users:(("node",pid=30724,fd=21))
Running it on a lab box? Forward the port. Moving the bind is not really an option anyway, because the web app rejects --host 0.0.0.0 outright and says all-interfaces binding is not supported yet. One SSH tunnel from your workstation and the UI behaves exactly as it does locally:
ssh -N -L 3080:127.0.0.1:3080 [email protected]
First load throws up a notice that the project is in testing for harness developers. Read it and click Continue; it does not come back.

Now the model. Open Settings then Models, paste a key from platform.deepseek.com into the DeepSeek card and save. The route becomes usable on the next request with no restart. Keys are write-only in the UI: the page gets a redacted descriptor back, never the secret, and the value lands in $DSH_HOME/.credentials.yaml while settings keep only a reference to it.

For a headless box or a CI runner there is no page to click, so hand the key in through the launching environment. That layer sits at the top of the credential order and is visibly read-only, so it beats anything stored on disk, which is exactly what a per-run override should do:
DEEPSEEK_API_KEY=your-key-here dsh --profile headless "summarise this repository"
Get the direction right, because it is counter-intuitive. A key in $DSH_HOME/.env is the lowest layer, below the managed store, so once you have saved a key through the Models page a stale one in that file is silently ignored rather than winning.
Add provider covers Anthropic, OpenAI, Bedrock, Vertex, Azure and Codex from the shipped catalog. Add a custom provider is the one that matters for a self-hosted setup: give it a lowercase provider ID, a base URL such as http://10.0.1.50:11434/v1 for an Ollama host, the openai-completions protocol and at least one model id. Plain OpenAI-compatible servers usually need two compatibility switches on top of that, which is a long enough story for its own guide.
Pick a workspace and run the first task
The composer stays locked until a workspace is selected, which trips people up on a fresh install. Click Choose workspace, add the directory you started dsh in, select it, and the input box comes alive.
The test project was deliberately broken. A port parser with the classic Python range mistake, on line 9 of a 15-line file:
def parse_ports(spec):
out = []
for part in spec.split(","):
if "-" in part:
a, b = part.split("-")
out.extend(range(int(a), int(b)))
else:
out.append(int(part))
return out
The task was to fix the off-by-one so that a range like 80-83 includes 83, then run the script to prove it. Four steps later the agent had read the file, patched line 9, executed the script and reported back with the output inline.

Each tool row expands in place, so the Bash row shows the working directory, the command and its output without leaving the conversation. The file on disk changed the way you would want a colleague to change it:
out.extend(range(int(a), int(b) + 1))
Running it by hand afterwards confirms the same result the agent claimed, which is the only verification that counts:
python3 ports.py 80-83
83 is now in the list:
[80, 81, 82, 83]
Total elapsed from prompt to patched file was seven and a half seconds, and the diff is four characters.
Read the trajectory when the agent does something odd
The Chat tab is the polite view. The Trajectory tab next to it is the one to reach for when a run goes sideways, and it is the part of the web UI worth learning before you need it. It lays the turn out as a timeline with input, model and tool lanes across the top, then every message in order: the system prompt, your prompt, the runtime context injection, each assistant turn, and each tool call with its raw JSON arguments and the raw result.

Seeing the exact edit payload, with the old string and the new string the model asked for, ends most arguments about what an agent “decided” to do. The Session log button in the top right downloads the same events as the JSONL file described earlier, which is the format to parse if you want to build reporting on top of agent runs.
The four built-in agent presets
A preset is the plugin composition a session runs: its tools, its prompt, its capabilities. Four ship with the package and you switch between them per session, or duplicate one and edit it.
| Preset | ID | What it composes |
|---|---|---|
| Standard mode | standard | Full coding agent: file editing, shell, file and web search, skills, planning, goals, subagents, workflows |
| PTC mode | code | Everything Standard has, exposed through the Code Mode SDK so the model writes one TypeScript program instead of many separate tool calls |
| Minimal mode | minimal | Two tools only: a persistent bash and str_replace_editor |
| Creator mode | cordis | Standard plus runtime inspection, plugin experiments and preset-authoring guidance, for building your own presets |

PTC mode is the interesting one for anyone watching token budgets. Collapsing a multi-step operation into a single generated TypeScript program cuts the round trips, which is the same lever behind the tooling in our guide on reducing agent token usage.
Minimal mode carries a sharp edge that is easy to miss. Its persistent bash goes through the sandbox policy, but its str_replace_editor talks to the local filesystem service directly and can address any absolute path the runtime process can see, whatever the sandbox mode says. That is a deliberate upstream design choice for a bare two-tool runtime, and it means Minimal mode is the wrong preset for an unattended job on a machine you care about. Run it in a disposable checkout or a container.
Run one task without the browser
The headless profile answers a single question in a fresh persisted session, prints the final answer and exits. No UI, no daemon:
cd ~/demo-app
dsh --profile headless "In one sentence, say what parse_ports returns for the spec 80-83,443. Do not edit any file."
Standard output is the answer and nothing else, which makes it pipe-friendly:
`parse_ports("80-83,443")` returns the list `[80, 81, 82, 83, 443]`.
The session still lands in $DSH_HOME/sessions/ exactly as a UI session does, so a cron job and a browser session are the same thing to the storage layer.
What the sandbox actually blocks
Three permission presets ship, and each one binds a sandbox mode to an approval policy. The default for a new session is the middle one.
| Preset | Sandbox mode | Approval policy | Effect |
|---|---|---|---|
| Read only | read-only | ask | No writes anywhere, including the workspace |
| Workspace write | workspace-write | ask | Writes confined to the session workspace root |
| Danger full access | danger-full-access | never | No confinement and no prompts |
Note what the third row really says. Turning the sandbox off also turns the approval prompts off, so there is no second line of defence behind it. Set the mode per session in the composer, or per process with an environment variable:
DSH_PERMISSION_MODE=read-only dsh --profile headless "audit this repo and report findings"
Enforcement is real rather than advisory. On Linux dsh reaches for bubblewrap first and falls back to Landlock, and the Landlock launcher ships inside the npm package as a prebuilt native binary rather than something you compile:
ls /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/node-addon-landlock-run-linux-x64/bin/
Asking a workspace-write session to write outside its workspace produces a kernel-level refusal, not a polite decline from the model:
[stderr]
bash: line 1: /home/jmutai/outside.txt: Read-only file system
[sandbox: file access denied under workspace-write mode]
[sandbox: escalation available - retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]
[exit code: 1]
The agent gets one sanctioned escalation attempt, and it has to name the narrowest mode that would work plus a justification, which then goes to you as a prompt. In headless mode there is nobody to prompt, so it fails closed with “no approval channel is available” and the file is never created. That is the behaviour you want in CI.

If neither backend is usable the harness raises SANDBOX_UNAVAILABLE instead of quietly running your agent unconfined, which is the right call and the opposite of what most tools do.
Where dsh is rough right now
Worth knowing before you wire it into anything that matters. The version story is the first snag: npm latest is rc.7 while the repository and the next tag are on rc.8, and the project has openly promised breaking changes, so record the exact rc a working config was tested against. The English documentation is thin compared to what is in the source tree, and several answers only turn up in package READMEs and architecture notes rather than the user guide.
Two smaller things are worth knowing up front. A bare npx @deepseek-ai/dsh sits silent for a minute or more on first run while it fetches the dependency tree, long enough to look hung. And dsh plugin forwards to pnpm inside the profile directory, so managing out-of-tree plugins means having pnpm around even though the base install never asked for it.
None of that is unusual for a preview that is a week old. The pieces that matter are already solid: the sandbox refuses correctly, the session log is honest, and swapping the model route is a config change rather than a fork. Next in this series we put dsh side by side with Claude Code on the seams that actually differ, then wire it to a local model so no prompt leaves the box, which pairs naturally with running DeepSeek V4 Flash locally and with the tool-plumbing tradeoffs covered in MCP vs RAG vs agent skills.