DeepSeek Harness (dsh): I Installed DeepSeek's Open-Source Coding Agent, Wired It Into My Chat App, and Benchmarked It
- Category
- AI & Local LLM
- Posted
- August 18, 2026
- By
- Jacob Lloyd β written with AI assistance, post-project
- Read time
- 14 min read
In plain terms: DeepSeek released a free, open-source 'coding agent' β a program that reads your project, edits files and runs commands for you, like Claude Code. This article shows how I installed it on my Linux PC, made it start automatically, connected it to my home chat app, and tested it on small real coding jobs with both DeepSeek's cloud models and a model running on my own hardware. It worked, it was cheap, and there are a few sharp edges.
On August 13, 2026 DeepSeek released DeepSeek Harness β dsh β an MIT-licensed coding agent built on the idea that everything is a plugin: the model adapter, the tools, the sandbox, even the agent loop. It hit 95,000 GitHub stars in two days. I installed it on my home box the same week, gave it a seat next to Reasonix, my existing coding agent, inside DisPatch, pointed it at both DeepSeek's cloud models and a model on my own GPU rig, and ran it through a small task suite. This is what that took, what it cost, and where it bit.
tl;dr
- What it is: a Claude-Code-class agent (reads/edits files, runs shell, keeps a plan, spawns subagents) shipped as an npm package. Two modes: a web UI on
127.0.0.1:3080and a headless one-shot mode that prints one answer and exits β built for scripts and other agents. - What it costs: the software is free. My whole 5-task benchmark cost about 3Β’ on V4-Flash and about 7Β’ on V4-Pro at peak rates; nothing on the local model.
- What you need: Node.js 22.19+/24, a DeepSeek API key or any OpenAI-compatible server (I used both).
- What you end up with:
dsh --profile headless "fix the failing test"from any project folder, a browser UI as a background service, model switching by editing one YAML file, and β in my case β a "DeepSeek Harness" bot in my chat app with a Run button. - The result: 15/15 passes across V4-Flash, V4-Pro and a local Gemma-4-26B, 2β18 s per task. Developer preview β the README warns in capitals that things will break between versions, and I hit a few of the sharp edges listed below.
The route, basic to advanced
Steps 1β3 get you a working agent in ten minutes. Steps 4β5 are what I did on top so other software β and other agents β can call it.
What you end up with
The thing I actually use is the headless mode. From inside a project directory:
$ cd ~/Projects/dsh-playground
$ dsh --profile headless "Create fizz.py that prints FizzBuzz for 1..15 and run it; reply with the program output only."
1
2
Fizz
4
Buzz
β¦
FizzBuzz
$ echo $?
0
Six seconds, file created, program run, answer printed, exit code 0. It writes nothing outside the folder you started it in (default permission mode workspace-write), prints only the final message on stdout, and persists every run so you can open it later in the web UI and read exactly what it did β every tool call, every token count.
The browser UI is the familiar 2026 layout β sessions left, chat middle, a settings page for models β with one good habit: the API key is pasted into the settings page, never a config file. Mine runs as a systemd user service and is embedded in my chat app next to Reasonix:
Step 1: install (two minutes)
One npm package. I keep a private Node prefix for agent tooling so nothing lands in the system tree β on an immutable distro like Bluefin that is the only sane place for it anyway β but a plain global install is the same command:
npm install -g @deepseek-ai/dsh
dsh --version # 0.1.0-rc.7 at the time of writing
dsh web # starts the UI, prints http://127.0.0.1:3080
Open the URL, go to Settings β Models, paste your DeepSeek key, save. That writes ~/.dsh/.credentials.yaml (mode 0600) and the model route works immediately, no restart. To script it, the file is a plain YAML mapping β DEEPSEEK_API_KEY: sk-β¦ β and I wrote mine from the env file my other services already read, so the secret exists in one more place but never in a shell history or a unit file.
Two things worth knowing before you go further:
- Telemetry is off by default (
DSH_TELEMETRY_MODEunset = disabled). I checked the shipped config, not the marketing copy; the OTLP exporter exists, it is just not on. - The sandbox is real but narrow.
workspace-writeconfines writes to the directory you launched from. Reads are not confined β the docs say so plainly β so do not launch it from your home directory for a real task, and do not point it at folders holding secrets.
Step 2: run it as a service
The web UI is a long-running Node process; I wanted it up at login, no terminal, loopback only. A systemd --user unit does it:
# ~/.config/systemd/user/dsh-web.service
[Unit]
Description=DeepSeek Harness web UI (dsh web) on 127.0.0.1:3080
After=network.target
[Service]
WorkingDirectory=%h
Environment=DSH_HOME=%h/.dsh
Environment=DSH_PERMISSION_MODE=workspace-write
ExecStart=/usr/local/bin/dsh web --host 127.0.0.1 --port 3080 # `which dsh`
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now dsh-web.service
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080/ # 200
WorkingDirectory is the web UI's default workspace root, but the UI still makes you pick a workspace explicitly before it lets you type. Good default. The CLI also refuses --host 0.0.0.0 β the authors call all-interfaces binding "intentionally not supported yet", and since the UI has no auth at all, I agree. Want it from another machine? Put an authenticating reverse proxy in front, or reach the host's own browser over your remote-desktop path (what I do).
Step 3: models β one YAML file, hot-reloaded
~/.dsh/settings.yaml holds the default model and any extra providers, and dsh re-reads it for the next request β no restart, no re-login. Mine:
agent-default-model:
provider: deepseek-official
model: deepseek-v4-flash # or deepseek-v4-pro
llm-deepseek:
reasoningEffort: high # off | low | high | max
# A local OpenAI-compatible server (mine is llama.cpp-based on a GPU rig).
llm-pi-ai:
providers:
buildpc:
displayName: StudioForge (GPU rig)
apiKeyEnv: STUDIOFORGE_PLACEHOLDER_KEY # a reference, not a value
api: openai-completions
baseURL: http://my-gpu-rig:1234/v1 # your server; mine sits on the tailnet
defaultContextWindow: 65536
models:
- id: unsloth/gemma-4-26B-A4B-it-qat-GGUF/gemma-4-26B-A4B-it-qat-UD-Q4_K_XL
name: Gemma 4 26B-A4B (rig)
Two details that cost me ten minutes each: apiKeyEnv is a reference resolved from .credentials.yaml or the environment, never a literal β and a keyless local server still needs some credential referenced, because the OpenAI-compatible client insists on a bearer token. A placeholder value in .credentials.yaml satisfies it. And the provider ID (buildpc) is permanent once sessions reference it; rename by adding a new one.
Because the file is the whole interface, "switch dsh to the local model" is a two-line edit any script β or any other agent β can make. My lead agent retargets the harness to a free local model for chores and back to V4-Pro for hard problems without touching anything else.
Step 4: next to Reasonix in DisPatch
I already had Reasonix living inside DisPatch as a pseudo-bot: click it and the chat pane becomes a terminal running its TUI over a PTY. I wanted dsh beside it. The catch: the official package has no TUI. Third-party "Claude-Code-style TUI" plugins exist on npm, but they were four days old with broken workspace:* dependencies, and an unreviewed package with shell access does not go on a family server. So the dsh pane is built from what the official package does give you:
- the web UI embedded in an iframe (it sends no frame-blocking headers), shown only when the browser can reach the host's loopback β a phone on the tailnet gets a plain explanation instead;
- Start / Restart / Stop for the systemd unit, plus health;
- a Model dropdown that rewrites
agent-default-modelin that YAML file; - a Headless jobs tab: type a task, pick a folder under home, press Run. The server spawns
dsh --profile headless "β¦"as a fixed argument list (no shell β the task is one argv element, so a task containing; rm -rf /is just text), one job at a time, and keeps a short history with each final answer.
dsh --profile headless run; the answer is the last thing dsh printed. Three real runs: 5 s, 5 s, 19 s.Everything in that pane sits behind the app's admin unlock β a headless job is arbitrary code execution, and the locked family devices never learn the pane exists. If you wire dsh into anything of your own, copy that: treat "run a task" exactly the way you treat a shell.
dsh vs Reasonix, side by side
Both are Claude-Code-shaped agents that bill per token on DeepSeek. They differ in who made them and what surface they give you:
| DeepSeek Harness (dsh) | Reasonix | |
|---|---|---|
| Who | DeepSeek, official, MIT | Third-party, Claude-Code-style |
| Surface | Web UI + headless one-shot; no TUI | Terminal TUI (interactive session in a PTY) |
| Call it from a script | dsh --profile headless "β¦" β one answer, exit code | Designed for a human in the loop; scripting is awkward |
| Model switch | Edit settings.yaml, hot-reloaded; no --model flag | config.toml tiers (flash/pro) + per-skill routing |
| Local models | Any OpenAI-compatible server via a provider block | Same triple: base URL, key env, model ids |
| Extensibility | Everything is a plugin (model, tools, sandbox, loop) | Subagents, skills, per-project memory |
| In my DisPatch | iframe + unit controls + headless jobs tab | xterm.js terminal over a PTY |
| Maturity | Developer preview (rc.7), breaks between versions | v1.18, self-updater, settled config |
In practice: Reasonix is what I open when I am going to sit with the agent; dsh is what my other software calls. That split is why both stay.
The benchmark: 5 tasks, 3 models, 15/15
Nothing scientific β five small tasks I would actually hand a coding agent, each in a fresh scratch folder, each checked automatically (does the file exist and run? do the tests pass without touching the test file? did the rename leave zero old references?). Wall time is the whole process including its ~7,500-token system prompt; tokens come from dsh's own session log.
| Task | V4-Flash | V4-Pro | Gemma-4-26B (local, rig) |
|---|---|---|---|
| Reply "PONG" (boot + one call) | β 2.1 s | β 2.8 s | β 13.2 s* |
| Write + run FizzBuzz | β 5.3 s Β· 2 tools | β 8.4 s Β· 2 tools | β 4.9 s Β· 2 tools |
| Fix 2 bugs so unit tests pass (tests untouched) | β 11.7 s Β· 8 tools | β 15.8 s Β· 7 tools | β 10.4 s Β· 8 tools |
| Summarize a 6-module codebase (<150 words) | β 8.9 s Β· 9 tools | β 10.9 s Β· 7 tools | β 12.1 s Β· 7 tools |
| Rename a function across 3 files + tests, prove green | β 15.1 s Β· 14 tools | β 18.2 s Β· 12 tools | β 12.2 s Β· 11 tools |
| Total wall time | 43 s | 56 s | 53 s |
| Tokens (input miss / cache-read / output) | 42.6k / 136k / 4.5k | 41.4k / 107k / 3.2k | 40.5k / 237k / 5.6k |
| Cost at peak rates (off-peak is half) | β $0.027 | β $0.072 | $0 (electricity) |
*first call after the model was cold-loaded on the rig; later tasks show the warm speed. Prices from DeepSeek's pricing page on 2026-08-18: Flash $0.014 / $0.44 / $1.32 per million (cache hit / miss / output), Pro $0.044 / $1.32 / $3.96.
What the table says:
- Prompt caching carries the bill. Every task pays ~7.5k tokens of system prompt, but after the first step it is nearly all cache reads at 3% of the miss price. Multi-step tasks are cheap because the harness keeps the prefix stable.
- Pro used fewer steps and fewer tools for the same result (12 vs 14 tool calls on the rename; 3 vs 4 steps on the summary). On this suite Flash was faster and a third of the price, so it stays my default.
- The local model held its own. Gemma-4-26B (a 4B-active MoE, Q4 quant, served by llama.cpp on two RTX 5090s) passed everything, with more steps and more tokens but competitive wall time. First time a local model has been a real option for chores here rather than a novelty β though five small tasks say nothing about a 40-file refactor.
Then the "normal stuff" test: I pointed it at this website's repository and asked it to read the project's runbook, run the build-and-link-check, and report β no publishing, no edits. It read the runbook, ran the right command, reported the exact checker lines (290 pages, 1,752 images, no broken links), the build time, and β unprompted β noticed the runbook still said "288 pages" and flagged the drift. Fifty-three seconds. That is the low-stakes chore I hand off all day.
The fun test: build me a toy
Benchmarks are one thing; I also wanted to see what it does with an open-ended creative brief. So: "make an interactive pixel-art version of this site's logo β vanilla JS, embeddable anywhere, hover does something physical, click does something cool, no dependencies, test it yourself." Here is what it built β it is live, so go ahead:
prefers-reduced-motion.
How it went, honestly:
- Attempt 1 (V4-Flash): a 10-minute loop, no files. The brief allowed either a hand-drawn bitmap or a procedural one. It chose to hand-draw a 40Γ40 bitmap inside its reasoning and fell into a degenerate loop β the session log is hundreds of lines of
################/....β until my timeout killed it. About 2Β’ wasted. Lesson: never let a model hand-draw pixels in its head. - Attempt 2 (V4-Flash, brief amended to "rasterise from geometry, no bitmaps"): 25 minutes, everything delivered. 100 model steps, 201 tool calls, 172k output tokens (123k of them reasoning), 16.3M cache-read tokens β β 49Β’ at peak rates. It wrote a 399-line
ll-pixel-logo.jswith a one-global API anddata-knobs, a demo page, a README, a Node unit test for the rasteriser, and β unprompted β a Playwright script that screenshots the demo at three sizes. It hit my 25-minute cap while polishing the README, so the exit code was a timeout, but the work was done. Exit codes lie both ways. - The art: the ring and two slanted Ls read as the mark at a glance; they are chunkier and more Z-like than the real logo, and I would spend ten minutes tuning its shear constants before using it in anger. I did not β what you see is untouched.
- Embedding it here took one line because the site's CSP is
script-src 'self'and the widget makes no network requests. That was in the brief; it honoured it.
Gotchas
- Developer preview, and it says so in capitals.
0.1.0-rc.7at install; rc.6 was three days earlier. Profiles, config keys and the plugin layout can change. Pin the version in anything you automate and re-run a smoke test after every upgrade. - Headless is silent until it finishes. Nothing streams to stdout β a long task looks hung. Read the session log (
~/.dsh/sessions/β¦/session.jsonl.zstd, zstd-compressed JSONL) or watch it in the web UI. And exit code 0 means "the turn completed", not "the task succeeded". Check the work. - No
--modelflag in headless mode. The default model comes fromsettings.yaml; change it there (hot-reload) or in the UI. - Keyless local servers need a placeholder credential referenced by
apiKeyEnv, and provider IDs are permanent. Covered above; it will bite you. - The web UI's assets are absolute-pathed (
/assets/β¦,/api), so you cannot mount it under a sub-path behind your own reverse proxy without rewriting; embed it or give it its own hostname. - The UI locale follows your browser β the shipped index.html says
lang="zh-CN", and the first thing you see is an "Internal Testing Notice" dialog. Click Continue; everything after that was English for me. - The install runs postinstall scripts (node-pty, koffi, protobufjs). npm warns about it. Nothing malicious that I could see, but it is native code compiling in your prefix β another reason to keep it in a private prefix, not the system one.
- Node 22.19+ or 24 only. Older LTS refuses to run it.
Where this leaves me
It earned its place in an afternoon. Headless mode is the right shape for a coding agent other software calls β one command, one answer, one exit code, an auditable log β and model-by-YAML means my agent stack can point it at whatever brain fits the job. If you already run a local model server, try the same five tasks; ten cents of API credit and a scratch folder is the whole commitment.
Related: Reasonix (the other coding agent in this article), DeepSeek Everywhere (wiring DeepSeek into Claude Code and an agent stack), DisPatch (the chat app the pane lives in), and bench-llm (benchmarking local models more seriously than I did here).