跪拜 Guibai
← Back to the summary

DeepSeek Harness: Local AI Agent Writes a Working CLI Tool in 11 Steps, 3 Tests Green

Let me start with the conclusion: DeepSeek Harness works, and it's the shortest path from install to a working local agent I've tried in the past two weeks.

But the pitfalls are not in the features; they're in the environment. I installed it from scratch on a clean Ubuntu 22.04 and hit two problems, one of which made me think the software was broken.

The complete process from install to use follows. All numbers are from real runs; the screenshots are from my actual server session.


1. What it is

In one sentence: DeepSeek Harness is not a model; it's the shell that runs models.

The model (like DeepSeek-V4.1-Flash) does the thinking; the Harness wires things together: reading and writing files, executing commands, maintaining task plans, managing sessions, controlling permissions, and managing plugins.

You can think of it as the "cockpit" in an IDE: the model is the engine, Harness is the steering wheel, accelerator, and instrument panel. You can swap engines and keep driving.

Key facts:

Item Value
Origin Officially open-sourced by DeepSeek AI
License MIT
Language TypeScript
Version 0.2.1-rc.5 (preview, official notice says breaking changes are expected)
Architecture everything-is-a-plugin, built on Cordis
Popularity About 270k stars

That "everything is a plugin" is not marketing. Its model providers, tools, terminal, UI panels, even scheduled tasks, are all plugins. That's why the plugin page looks like an app store rather than a toggle list.


2. Before installing: the Node version gate

The official README only says "Install Node.js." That sentence is a trap.

On my machine, node -v showed v20.11.0, which looked modern, but running it blew up immediately:

node:util does not provide parseEnv on Node 20

SyntaxError: The requested module 'node:util' does not provide an export named 'parseEnv'

parseEnv in node:util only exists in Node 22 and later. So:

dsh requires Node 22 or higher. Node 20 installs fine but fails on first run with an error that gives no hint it's a version problem.

I switched to Node 24 with nvm. If you have other projects on the machine, don't change the global Node; use nvm with per-directory switching:

nvm install 24
nvm use 24
node -v          # v24.18.0

3. Installation: two methods

Method A: direct npx (try it first)

npx @deepseek-ai/dsh web

No installation, no global pollution. Good for "just want to see what it looks like."

Method B: global install (long-term use)

npm install -g @deepseek-ai/[email protected]

But the first time I installed this way, the compiled output was missing. The cause: npm 11 changed its default policy — install scripts are blocked by default, and it only prints a warning and continues.

npm 11 blocks install scripts

Blocked packages include node-pty (terminal emulation) and koffi (native calls) — packages that need to compile native modules. The symptom: the software “installed”, but the terminal feature was empty.

The correct way is to explicitly allow those packages:

npm install -g @deepseek-ai/[email protected] \
  --allow-scripts=@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs

After install: 616 packages, 21 seconds. Verify version:

dsh --version     # 0.2.1-rc.5

These two pitfalls (Node version, install script) both fall under “installs fine but doesn't work right,” and neither is in the README. Remembering them can save an hour.


4. Starting: the token is not optional

dsh web --no-open --port 3080

Startup prints a single line:

dsh web: http://127.0.0.1:3080/?token=jCprZH_J22hgMIqXx1OJrDblIL0wtOYfil4y9sKTdWQ

--no-open is for servers (no browser to open). By default it tries to open your local browser.

Don't skip that token. I tested three access methods:

port, token, memory check

Request Result
Direct access to 127.0.0.1:3080/ 401
With ?token=... 303 redirect to session page
Port listening scope Only 127.0.0.1:3080

Two layers of protection: only loopback address (no LAN access), plus a one-time token that becomes a cookie.

So if you plan to run it on a server and access it from your own computer, the correct way is SSH port forwarding, not opening to 0.0.0.0:

ssh -N -L 3080:127.0.0.1:3080 user@your-server
# then open the token-bearing URL printed by the server in your local browser

This is better than many similar tools. I've seen agent panels that default to 0.0.0.0:8080 with no auth — that's the real danger.

Measured resident memory: 308 MB.


5. Configuring the model: the key is write-only

Open the token-bearing URL; the first time you're asked to choose a model. Two entry points:

Add API Key

After saving, the model selector immediately shows DeepSeek-V4.1-Flash — no service restart needed.

model ready

One detail worth mentioning: the key is write-only. After saving, reopening the settings page shows an empty input, only saying “leave empty to use saved credentials.” The page never gets the plaintext key.

model settings page

The key ends up in $DSH_HOME/.credentials.yaml (permissions 600); the settings file only stores a reference. This design is far more sensible than “write the key plainly into config.json and echo it back to the frontend.”

To connect third-party backends (OpenAI-compatible gateways, internal gateways, self-hosted vLLM), go to Add Model Provider → Custom Model API: fill in Provider ID, base URL, protocol (OpenAI Chat Completions / Responses / Anthropic Messages, pick one), Key, then can “fetch available model list” to let the endpoint report itself. Provider ID cannot be changed after creation, so be careful.


6. Workspace: where it actually works

This step can confuse.

dsh's default workspace is not “the directory you started from.” It is:

~/Documents/deepseek-harness/default-workspace

The first time I asked it to write code, I searched for the output in ~/dsh-demo for a long time; find revealed the files were all under Documents.

To change: click “Default workspace” above the input box → Add workspace → choose a directory. Then all sessions can point to that directory.

This design makes sense: the workspace is an independent concept, separate from where the service starts, which is clearer for multi-projects. Just takes getting used to.


7. Meet three views before assigning work

Before giving it tasks, three UI elements are worth knowing; they determine how usable the tool is:

① Trajectory view. The session page has “Conversation / Trajectory ” tabs at the top. Conversation shows human-readable text; trajectory shows the raw parameters and returns of each tool call. When diagnosing “what did it actually change this file to,” trajectory is far more useful than conversation.

② This round's changes. Below the answer, it lists which files were changed in this session; clicking shows the diff with added/deleted line counts. My run showed “Edited 5 files +678,” each viewable individually.

③ Branch. Under each answer there's “Branch on new conversation” — it copies the context up to that sentence into a new session, so you can rephrase the question and retry without polluting the original thread.

Together these three solve an old problem: how do you confirm the agent didn't do something bad after it says it's done. Its own “completed” statement is not enough.


8. Hands-on: make it write a real tool

I don't like demonstrating agents with “write a Snake game.” So I gave it a task with acceptance criteria:

In this workspace, write a command-line tool that converts Markdown tables to CSV, with argument validation and 3 test cases; run it and show me the results.

How it ran

running

The interface breaks the process into categories: “thinking / run command / write / edit,” and each tool call's parameters and results can be expanded. This is critical: you can see why it did something, not just the final “done.”

One self-questioning moment is worth copying:

Bug: normalize rows uses width from selected[0] only. If multiple tables with different widths, using first table's width would truncate/pad all tables incorrectly. Better compute width per table... Fix: for each table, width = table.header.length.

After writing the first version, it noticed “using the wrong width when multiple tables have different widths” and went back to fix it. That bug is real: the selected[0] assumption only works for one table.

Result

session complete

Metric Measured
Turns / steps 1 round 11 steps
Files produced 5
Code changes +678 lines
Token usage 244K tokens, cache hit 95%
Speed 212 tok/s

The output works; I verified it separately:

tests all green

It also built its own argument validation matrix:

Input Handling
--bogus Unknown option: --bogus, exit code 2
--table 0 --table requires a positive integer, exit code 2
--table 9 Cannot find table 9 (document has 2 tables), exit code 1
does-not-exist.md Input file does not exist, exit code 1
input.md -o input.md Output file cannot be the same as input, exit code 2

CSV escaping is also correct:

姓名,年龄,城市
张三,30,北京
李四 | 小李,25,上海
项目,说明
早餐,"豆浆, 油条"
口号,"他说 ""你好"""

Fields with commas get quoted; fields with quotes use doubling. These are the easiest places to get wrong when converting GFM tables to CSV, and it didn't miss them.

One-sentence verdict: this task would take an intermediate engineer two hours; it took the agent less than two minutes, with comparable output quality.


9. Permissions: default doesn't let it leave the boundary

At the bottom left of the input box is “Access mode,” default is modify within workspace.

This default is key. It means: reads/writes and command execution are limited to the current workspace; touching system directories requires confirmation.

The three common modes:

Mode Behavior When to use
Read-only Can only view, not modify Read code, analysis
Modify within workspace (default) Free modification inside workspace, approval for outside Daily development
Full access No blocking Only when you fully trust the task boundary

I kept the default even on a disposable machine. I recommend not enabling full access on any machine with production credentials; an agent's accidental deletion is easier than you think.


10. Plugin system: this is what sets it apart

The plugin page splits into two sections: built-in plugin configuration and experimental plugins.

plugin page

Four built-in configurable items:

Four experimental items:

Plugin Purpose
Agent team Team collaboration, member list, shared task board
Voice input Local SenseVoice transcription, requires installing dependencies first
Auto authorize review Let the model decide whether to authorize each tool call
Developer tools View debug session raw data, message grouping, debug Node backend

Third-party plugins come via npm or Git; after install they become new tools/panels/model providers. This also means installing a plugin adds permissions to the agent; check the source.

MCP is also supported, via --patch to overlay:

dsh web --patch ./mcp-memory.yml

Attached tools appear in session as mcp__<service>__<tool>. The official repo provides ready-made configs for three memory systems (Memorix, MCP Reference Memory, Engram), all disabled by default.


11. Scheduled tasks: no need for cron

This is a feature I didn't expect. It provides four tools directly in the agent: schedule_create / schedule_list / schedule_view / schedule_edit.

You can just say:

Remind me every morning at 9 to check yesterday's deployment results

It will create a task with an RFC 5545 recurrence rule, and at the scheduled time deliver the instruction into a session (it can reuse the same session or create a new one each time).

scheduled tasks page

The task page shows next run time, frequency, delivery logs, and supports manual Run now, pause, delete.

One limitation to note: deliveries must be at least 1 minute apart (for tasks that create a new session each time, based on the Host's minimum run interval, default 1 minute). So don't expect second-level polling.

Better than crontab because: it delivers a “natural language instruction,” not a shell command; failures show the full process in a session, no need to dig through logs.


12. Where data lives, how to back up

Everything is under $DSH_HOME, default ~/.dsh:

~/.dsh/
├── .credentials.yaml        # API Key (600)
├── .anonymous-user-id
├── profiles/                # per-profile config layers
│   └── web/cordis.patch.yml # your settings changes land here
├── sessions/                # session records
└── storages/                # session artifacts

After install plus one session, the whole directory is 187 KB. Backup means copying this directory; migration means copying it to a new machine at the same path.

Want to change config without clicking the UI? Directly edit ~/.dsh/profiles/web/cordis.patch.yml, and the next request takes effect without restart.

The config file looks like (abridged) — essentially an ordered list of plugin patch layers, with your overrides at the end. That's how it can “change one config line to swap model provider.”


13. CLI mode: headless does one thing in 4 seconds

The Web UI is just one profile. The command line also has:

dsh web                                  # Web UI (default)
dsh tui                                  # terminal UI
dsh headless "run the tests"             # run one task, print result, exit
dsh --profile web --help                 # see all parameters for current profile
dsh --dump-config                        # print full merged config tree
dsh --patch ./extra.yml                  # apply an overlay patch (repeatable)

dsh headless is the most practical: drop it into CI, one prompt does one thing, exit. No service, no browser.

I ran a real task with it:

dsh headless "count lines in each file in the current workspace, sort by line count descending, output only the result"

The result — first it reported an error:

headless credential pitfall

dsh: MISSING_CREDENTIAL: llm-deepseek: no API key for provider route "deepseek-official";
store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it),
or export DEEPSEEK_API_KEY in the launching environment

This is the most memorable pitfall of the whole article: the key you configured in the Web UI is not readable by headless mode.

The two entry points store credentials differently: the Web interface writes to ~/.dsh/.credentials.yaml, headless only recognizes DEEPSEEK_API_KEY in the launching environment. Same tool, same config directory, but the credential path is not unified.

So in a script you must write:

export DEEPSEEK_API_KEY="sk-..."
dsh headless "count lines per file, sort"

After adding the environment variable, this same task ran in 4.019 seconds, outputting a result table directly:

| file | lines |
| bin/mdtable2csv.mjs | 400 |
| test/mdtable2csv.test.mjs | 149 |
| README.md | 83 |
| package.json | 23 |
| examples/sample.md | 23 |
| examples/sample.csv | 8 |

These 4 seconds cover reading files, running commands, generating the answer, and exiting. This speed is suitable for a Git pre-commit step or a CI validation stage.

Reminder: don't hardcode keys in scripts. Use CI secrets, or pass / 1Password CLI injection.

--dump-config is also worth remembering: when config doesn't take effect, first look at what it actually merged, which is faster than guessing through docs.


14. Three advanced configs worth knowing early

① Sub-agents: delegating work

The first item on the plugin page is “Sub-agents,” with three configurable fields: recursion depth, count limit, and which model each sub-agent uses.

This is key for cutting costs. A typical setup:

Role Model Reason
Main agent Strong reasoning model Decompose tasks, make decisions
Sub-agents Cheap, fast models Read files, run commands, batch rename

Have the main agent send a sub-agent to “extract header comments from 200 files in this directory” — that job doesn't need a strong model. Don't set the layer too deep; two layers are enough. With three or more layers you'll see token consumption grow exponentially, because each layer carries context.

② MCP: attach third-party capabilities

MCP doesn't need a plugin; use --patch to mount an overlay file:

dsh web --patch ./my-mcp.cordis.yml

Attached tools appear in session as mcp__<service>__<tool>, and the model can see and call them.

The official repo provides three ready-made memory configs (Memorix, MCP Reference Memory, Engram), all off by default — because these are third-party services, and DeepSeek explicitly says “provided as interoperability examples only, not endorsement or support.” Take that seriously: mounting an MCP means giving that service your data.

For a permanent effect, don't pass --patch each time; merge the overlay section into ~/.dsh/profiles/web/cordis.patch.yml. Note it's merge, not overwrite — that file may already contain your own changes.

③ Custom model provider: connect your company's gateway

Settings → Model → Add Model Provider → Custom Model API. Fields:

Protocol note: one provider can only speak one protocol. If your gateway supports both OpenAI and Anthropic formats, create two providers; you can't mix them in one.

After filling, you can click “Fetch available models” to let the endpoint report its model list. But the official docs also admit this is a “convenience feature,” not a guarantee — many gateways /models endpoints have non-standard formats; if fetch fails, fill manually, same effect.


15. Should you use it? My judgment

Good for you if:

Not great for:

Compared to similar tools, its differentiation is not in the model (everyone can plug in models), but in the thoroughness of pluginization and the permission defaults. The former determines what it can become in six months; the latter determines whether you'll let it touch your directories today.


16. Six pitfalls quick reference

Symptom Cause Solution
parseEnv error on first run Node < 22 nvm install 24 && nvm use 24
Terminal feature empty after install npm 11 blocked install scripts add --allow-scripts=...
401 when accessing 3080 directly missing token use the full ?token= URL from startup output
Can't find files it wrote default workspace is ~/Documents/deepseek-harness/default-workspace switch workspace, or look there
Config change not taking effect edited wrong file run dsh --dump-config to see merged result
headless says MISSING_CREDENTIAL Web UI's key not available to CLI export DEEPSEEK_API_KEY=...

One final question

The Markdown-to-CSV tool I had it build took 11 steps, 244K tokens, 95% cache hit. The cache hit rate is the most interesting number: it indicates most tokens are re-read context, not new generation.

So the question is: if you hand the same repository to the agent for ten consecutive tasks, does the cost after the first one approach only incremental increases?

I haven't run complete tests in this direction. If you've run similar consecutive tasks, share your token curve in the comments; I'll fold the data into a comparative review.


Related reading (same column):

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

卷土的土

The version compatibility section is very practical.