DeepSeek Harness (dsh): I Installed DeepSeek's Open-Source Coding Agent, Wired It Into My Chat App, and Benchmarked It

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.

Quick test before you read on: mouse over it, then click it. dsh built this pixel-art version of the site's logo from a written brief β€” vanilla JS, no dependencies β€” and the whole story of how that went is further down.

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:3080 and 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:

DisPatch chat app with the 'DeepSeek Harness' bot selected: the dsh web UI embedded in the main pane showing a finished session 'Create and run hello.py' with its Think, Write and Bash steps and the answer; a status bar below with Running, a Model dropdown set to DeepSeek-V4-Flash, and Start/Restart/Stop buttons
The dsh web UI inside DisPatch, showing a finished session with its step trajectory (Think β†’ Write β†’ Bash) and the per-turn stats line. The bar at the bottom controls the systemd unit and the default model.

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_MODE unset = 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-write confines 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-model in 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.
The Headless jobs tab in DisPatch: a task box, a working-directory field, and three finished job cards in ~/Projects/dsh-playground β€” 'Done in 19s' (add a docstring to fizz.py and re-run it), 'Done in 5s' (create primes.py, output 2 3 5 7 11…), 'Done in 5s' (list the files and describe each) β€” each showing the task and the answer text
Headless jobs from the chat app. Each card is one 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
WhoDeepSeek, official, MITThird-party, Claude-Code-style
SurfaceWeb UI + headless one-shot; no TUITerminal TUI (interactive session in a PTY)
Call it from a scriptdsh --profile headless "…" β€” one answer, exit codeDesigned for a human in the loop; scripting is awkward
Model switchEdit settings.yaml, hot-reloaded; no --model flagconfig.toml tiers (flash/pro) + per-skill routing
Local modelsAny OpenAI-compatible server via a provider blockSame triple: base URL, key env, model ids
ExtensibilityEverything is a plugin (model, tools, sandbox, loop)Subagents, skills, per-project memory
In my DisPatchiframe + unit controls + headless jobs tabxterm.js terminal over a PTY
MaturityDeveloper preview (rc.7), breaks between versionsv1.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.

TaskV4-FlashV4-ProGemma-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 time43 s56 s53 s
Tokens (input miss / cache-read / output)42.6k / 136k / 4.5k41.4k / 107k / 3.2k40.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:

Mouse over it, then click it. Pixels flow away from the pointer and spring back; a click shatters the mark into bouncing pixels that find their way home. Touch works too; it respects prefers-reduced-motion.
Four frames of the pixel logo widget: idle, pixels pushed aside around the cursor with a blue glow, a cyan cloud of pixels bouncing off the edges right after a click, and the pixels drifting back into the ring-and-two-Ls shape
Idle β†’ hover repulsion β†’ shatter β†’ reassembly, captured in headless Chromium. 61 fps, zero console errors, and it does reassemble.

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.js with a one-global API and data- 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.7 at 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 --model flag in headless mode. The default model comes from settings.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).


← More AI & Local LLM