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:
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.
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:
| 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:
- Log in to DeepSeek account
- Fill in API Key (I chose this)
After saving, the model selector immediately shows DeepSeek-V4.1-Flash — no service restart needed.
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.
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
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
| 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:
bin/mdtable2csv.mjs: 400 lines, zero dependenciestest/mdtable2csv.test.mjs: 149 lines, 3 test casesREADME.md: 83 lines- Total 632 lines, tests run in 328 ms
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.
Four built-in configurable items:
- Sub-agents: recursion depth, count limit, which model each sub-agent uses
- Web search: configure DeepSeek's search provider
- Agent loop: controls how the agent dispatches tool calls
- Terminal: maximum runtime per single command, maximum output (prevents
cata 10G log from exploding the session)
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).
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:
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:
- Provider ID (lowercase, cannot be changed after creation; requests, session records, credential references all use it)
- Display name (changeable)
- Base URL
- API protocol: OpenAI Chat Completions / OpenAI Responses / Anthropic Messages, choose one
- API Key
- At least one model
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:
- You want a local, data-stays-on-machine agent (sessions, files, keys all local)
- You already have a DeepSeek API key, don't want another subscription
- You like seeing “why it did that,” not just the final answer
- You need scheduled tasks, sub-agents, MCP, but don't want to assemble them yourself
Not great for:
- Production. It's labeled preview, and the official notice explicitly says breaking changes are coming. Fine for getting work done; not for entrusting critical processes.
- Team collaboration as a requirement. Multi-user permissions, auditing are not mature.
- People who refuse to touch the command line. Install and config still require typing commands.
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):
- AI Agent crashed, I recovered it with a snapshot in 20 minutes
- Set up a private Gitea with Docker in 10 minutes: HTTPS, SSH, CI, backup all configured
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
The version compatibility section is very practical.