What dsh is, and what actually happens when you run it
The product in one paragraph
DeepSeek Harness (dsh) is an open-source agent harness: the scaffolding that
turns an LLM into a working coding agent — tool execution, sandboxing, session
persistence, a web GUI, subagents, skills, hooks. Its one architectural bet is
that everything is a plugin on a vendored copy of the Cordis framework
(README.md:7). It is in developer preview and says so loudly:
README.md:11 — "THERE WILL BE COMPATIBILITY-BREAKING CHANGES." External
pull requests are not accepted (CONTRIBUTING.md:9), so this is
open-source-as-window, not open-source-as-community.
What actually ships
Three bundles exist in the box (packages/bundle/): base, web-app, and
headless. That yields exactly two usable surfaces:
dsh web— the full GUI, multi-session, per-session agent presets. This is the lead surface:apps/web/tests/has 93 test files against 12 for the CLI.dsh headless— a one-shot task runner. Its bundle is 35 lines and mounts "no Host, HTTP server, Web runtime, or browser plugin" (packages/bundle/headless/cordis.patch.yml:1-5).
The CLI binary itself is a launcher, not a product surface — its own README
says so (apps/cli/README.md:5: "The dsh command is the product launcher
for profiles"). There is no interactive terminal UI in the box. The --help
text and README advertise a tui profile (apps/cli/src/args.ts:68-71,
apps/cli/README.md:24), but no tui bundle exists and
PROFILE_TEMPLATES (packages/boot/app-boot/src/profile.ts:114-117) knows
only web and headless. The TUI is a third-party GitHub install
(apps/cli/reference/README.md:46: dsh plugin --profile tui add
github:deepseek-harness/turtle-ui). A first-run user who types the help
example gets a "profile does not exist" error.
npx @deepseek-ai/dsh web, demystified
Four hops, all in one Node process.
Hop 1 — the bin. The published package's only entry is a bundled ESM file
(apps/cli/package.json:14-19: "bin": { "dsh": "lib/bin.js" }). It parses
argv first, then dynamically imports the mode module
(apps/cli/src/bin.ts:27-38).
Hop 2 — web is a pure alias. There is no web-specific launcher code;
web resolves to --profile web (apps/cli/src/args.ts:156-168), and
unknown tokens pass through to the booted tree (args.ts:123-128).
Hop 3 — profile materialization, with side effects. Every boot writes to
disk before doing anything else: it rewrites the profile's root config file
(apps/cli/src/profile-boot.ts:98-103 — the rationale at :88-93 is that the
loader's write-back would otherwise duplicate bundle rows on the next boot),
and it rebuilds a symlink farm under $DSH_HOME/profiles/node_modules
containing the entire dependency closure of the installed package
(packages/boot/app-boot/src/profile.ts:223-226, :191-198) so out-of-tree
plugins share one Cordis instance. Consequence: a read-only $DSH_HOME breaks
boot outright.
Hop 4 — one Cordis tree from patch layers. A single root context boots
with layers applied in order: bundle patches → profile cordis.patch.yml →
$DSH_HOME/cordis.patch.yml → any --patch overlays
(apps/cli/src/profile-boot.ts:122-129, :248). The entire product — HTTP
server, agent loop, LLM adapters, persistence — is rows in this composition.
Process model
One process. No daemon, no Electron, no separate server. Code Mode runs in a
worker thread (packages/bundle/web-app/cordis.patch.yml:41-43); shell tools
spawn subprocesses; the workflow worker is disabled on Web. Signals: SIGTERM
exits 0, SIGINT exits 130 (apps/cli/src/profile-boot.ts:221-222).
Network posture
The server binds 127.0.0.1:3080 by default
(packages/bundle/web-app/cordis.patch.yml:115-120). The --host flag exists
but is effectively a no-op: the schema admits exactly two values
(packages/host/webserver/src/index.ts:61 —
z.union([z.const('127.0.0.1'), z.const('0.0.0.0')])), and the only
non-default one is rejected at the flag layer with an honest error
(packages/bundle/web-app/src/startup.ts:69-71: "--host 0.0.0.0 is
intentionally not supported yet for safety: it would expose remote code
execution to the network"). There is no authentication — only a
DNS-rebinding/CSRF fence that explicitly disclaims being one
(packages/client/connection/src/api-request-trust.ts:12-13: "this fence is
not an auth layer"). Loopback-only is the security model.
The browser talks to the server over three channels on one port: HTTP uplink
under /api (packages/client/connection/src/api-path.ts:8), a
WebSocket downlink only — client-to-server WebSocket messages are a
protocol violation (packages/client/connection/src/websocket-downlink.ts:47-50)
— and per-plugin browser bundles served at /plugins/<id>/client.js
(packages/bundle/web-app/cordis.patch.yml:145-148). The SPA shell is served
from the separately published @deepseek-ai/dsh-web-frontend package, with a
server-injected window.__DSH_BOOT__ manifest; running the frontend under
bare Vite is hard-failed on purpose (apps/web/vite.config.ts:7-17).
Where state lives on disk
Everything is under ~/.dsh (override with $DSH_HOME;
packages/util/home-paths/src/index.ts:12-18):
| Path | What it is | Evidence |
|---|---|---|
profiles/<name>/ |
profile package.json, cordis.yml, patch file | packages/boot/app-boot/src/profile.ts:152-168 |
profiles/node_modules/ |
the boot-time symlink farm | profile.ts:223-226 |
cordis.patch.yml |
machine-wide user config layer | apps/cli/src/profile-boot.ts:49-51 |
sessions/ |
append-only JSONL session event logs | packages/bundle/base/cordis.patch.yml:101 |
storages/ |
JSON key-value stores, mode 0700 | packages/storage/storage-json/src/index.ts:64 |
attachments/v1/ |
image attachments | packages/attachment/attachment-local/src/index.ts:53 |
.credentials.yaml (0600) + .env |
API keys | packages/credentials/credentials-local/src/index.ts:7-9, :394 |
settings.yaml |
user settings | packages/settings/settings-file/src/index.ts:56 |
.agent-presets/ |
user-authored agent presets | packages/preset/agent-presets/src/discovery.ts:41 |
AGENTS.md |
user-global agent instructions | packages/context/agent-instructions/src/render.ts:93-94 |
Two details worth knowing. Credentials get real permission enforcement on
POSIX — boot refuses to start if the file is readable beyond its owner
(credentials-local/src/index.ts:117-119) — but the check is skipped on
Windows (:113: "its ACLs are not expressible here — so the check is skipped
rather than faked"). And the session search index on Web is configured
:memory: with openAt: never (packages/bundle/web-app/cordis.patch.yml:31-33),
so sidebar search matches session titles and workspace names only, not
content.
Platform support, honestly
| Target | dsh runs |
kernel sandbox | Python SDK runtime |
|---|---|---|---|
| linux-x64 / arm64 | yes | yes (bwrap → Landlock) | yes |
| macos-arm64 | yes | yes (Seatbelt) | yes |
| macos-x64 | yes | yes (Seatbelt) | no |
| win32-x64 | yes | partial (restricted-token ACL) | no |
Windows is a real engineering target, not an afterthought: the base bundle
swaps the whole shell family per platform in config
(packages/bundle/base/cordis.patch.yml:210-216: tool-bash disabled on
win32, tool-pwsh disabled elsewhere), there is a CVE-2024-27980 spawn
workaround (apps/cli/src/plugin.ts:127-132), junction-aware symlinking, and
three Windows CI lanes. But the blocking Windows CI signal runs under Wine
on Ubuntu, not real Windows (see how-they-build-it.html),
and the Windows ACL sandbox self-reports as structurally partial
(packages/sandbox/sandbox-local/src/index.ts:181-186: NTFS hard links can
alias a granted file outside the workspace).
The Python SDK (python/sdk) drives the harness as a subprocess over stdio
JSON-RPC, but it is unreleased — python/sdk/pyproject.toml:7 says
version = "0.0.0.dev0" — and defines wheels for only three platforms
(python/sdk-runtime/platforms.json:1-13): no Windows, no Intel Mac.
Traps for a new user
dsh pluginsilently requires pnpm. The plugin command shells out topnpm(apps/cli/src/plugin.ts:129) and fails with exit 127 if it is missing. The install story (README.md:19-21) mentions only Node and npx.- Errors sometimes speak contributor. If the frontend dist cannot be
resolved, an npx user is told to "run pnpm run build from the repository
root first" (
packages/bundle/web-app/src/index.ts:122) — a repository they do not have. - The user guide is thin.
docs/user/guide/index.mdis 30 lines covering four steps. Sessions, presets, permission policy, plan mode, subagents, and Code Mode — all mounted in the shipped web bundle — are undocumented for users. - The web shell declares itself Chinese.
apps/web/index.html:2is<html lang="zh-CN">for every user, despite an English onboarding pass.