Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add @tr1v3r/dsh-proxy
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
中文说明见 README.zh.md。

@tr1v3r/dsh-proxy is a DeepSeek Harness plugin that routes every
in-process outbound request — LLM providers, web_search / web_fetch,
streamable-http MCP — through an HTTP(S) CONNECT or SOCKS5 proxy, and lets
you flip the proxy on, off, or to another server at runtime, with zero
restarts, either from the Web Settings → General → Network proxy control or
by editing the dsh-proxy entry in the profile's cordis.patch.yml (hot-reloaded).
The demo above shows the routing engine: node scripts/demo.mjs after install.
How it works
DSH and pi-ai issue requests through globalThis.fetch, which reads undici's
well-known global dispatcher slot (Symbol.for('undici.globalDispatcher.1')).
The plugin owns that slot:
http(s)://proxy →EnvHttpProxyAgent(CONNECT tunneling for https)socks5://proxy → undici's built-inSocks5ProxyAgent(URL credentials supported;socks5h:///socks://normalize to it; DNS resolves remotely)noProxyrules → both paths route through oneRoutingDispatcher, so HTTP and SOCKS share identical matcher semantics (undici-style: bare entries match the host and dot-boundary subdomains;host:portpins a port;*bypasses everything; a leading dot or*.prefix is accepted as a synonym of the bare entry). Inmanualmode, ambientNO_PROXY/HTTP_PROXYenv vars are deliberately ignored by the dispatchers — exported env only steers child processes, so in-process routing is fully determined by the profile entry config.systemmode is the opposite: it follows the ambient proxy —HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXYenv vars, falling back to the macOS System Settings proxy (scutil --proxy) — re-detected each time the section is applied, not continuously polled.
With exportEnv: true (default) the switch also exports
HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY into the dsh process, so
child processes spawned after the switch (bash-tool curl/git, stdio MCP
servers) follow the same proxy. Variables you set yourself at boot are never
clobbered, and everything is restored on disable/unload. While loopback bypass is
on (the default), the exported NO_PROXY also gains the loopback defaults
(localhost,127.0.0.1,::1, merged and deduplicated with your rules); with
bypassLoopback: false your list is exported unchanged.
Retired dispatchers close gracefully and are force-destroyed after 30 s, so
switching away actually tears down old keep-alive connections. In-flight
requests get that same 30-second grace period (RETIRE_DESTROY_MS); a
streaming response that is still running ~30 s after you flip the switch is
interrupted when the retired dispatcher's sockets are force-destroyed.
Credential security
Proxy URLs with embedded user:pass@ credentials are stored in
plaintext on disk — in the profile's cordis.patch.yml and the settings
persistence — and are protected only by file permissions. With the default
exportEnv: true, the credentials are also propagated into the dsh process
as HTTP(S)_PROXY env vars, so every child process spawned afterwards
carries them (ps -E on Linux/macOS or /proc/<PID>/environ can reveal
them to the same user; other users generally need root or comparable
privileges to read them, depending on platform permission settings).
No new config option is introduced for this; if
your credentials are sensitive, prefer pointing dsh-proxy at a local,
unauthenticated proxy entry (e.g. http://127.0.0.1:7890 in front of an
authenticated upstream) instead of embedding user:pass@ in the URL.
Install
In the target profile directory (~/.config/dsh/profiles/<name>/):
Add the dependency and bundle in
package.json:{ "dependencies": { "@tr1v3r/dsh-proxy": "^0.2.4" }, "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@tr1v3r/dsh-proxy"] } } }(Merge the bundle into your existing
dsh.profile.bundleslist.)Install:
dsh plugin --profile <name> install --no-frozen-lockfileRestart dsh once to mount the plugin; afterwards never again — switching happens through settings.
Use
In the Web profile, use the icon-only Proxy status control in the sidebar footer (above Settings) to check the selected mode on hover/focus or in its menu and switch between Direct, Follow system, and Manual proxy without leaving the main screen. The menu uses the same settings namespace and updates when the profile patch changes externally. The tooltip shows the manual endpoint with credentials masked; Follow system reflects the selected mode, not a guarantee that the host detected a usable proxy (consult DSH logs for the effective route). A missing manual URL cannot be activated from the quick menu. To edit the URL, bypass hosts, or child-process export, open Settings → General → Network proxy.
In Settings → General → Network proxy, select Direct,
Follow system, or Manual proxy from the dropdown; mode changes apply immediately,
without an Apply button. In Manual mode, enter an HTTP(S)/SOCKS5 URL and bypass
hosts (one per line); text fields save on blur, while the child-process env switch
saves on change. Invalid URLs are not saved. The UI writes the same dsh-proxy
profile entry. Editing the profile patch remains supported and refreshes the UI; a
revision fence prevents a stale edit from silently overwriting an external change.

The selector offers all three routing modes:

Alternatively, add this override to
~/.config/dsh/profiles/<name>/cordis.patch.yml (hot-reloaded, no restart).
If the file already contains entries, append this item to the YAML list, or edit
the existing dsh-proxy item.
The mode key picks direct, system, or manual:
- id: dsh-proxy
config:
mode: manual # direct | system | manual
proxy: socks5://127.0.0.1:1080 # manual only — http://…, https://…,
# socks5://user:pass@host:1080, socks5h://…
noProxy: # manual only — optional bypass list
- localhost
- .internal.example
- registry.corp:443
bypassLoopback: true # manual + system — loopback stays direct
# (false to proxy it)
exportEnv: true # manual only — also set HTTP(S)_PROXY for children
mode |
behavior |
|---|---|
direct |
No proxy — everything goes out directly (same as the old enabled: false). |
system |
Follow the host's proxy, detected each time the section is applied: HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY env vars everywhere, falling back to the macOS System Settings network proxy (scutil --proxy) when env is unset. It re-detects on settings save, not continuously; Windows registry, Linux-desktop and PAC are not yet covered. proxy/noProxy/exportEnv are ignored. |
manual |
Route through the proxy URL with the optional noProxy bypass list (same as the old enabled: true). |
enabled: true/false still works as a deprecated alias for
manual/direct when mode is omitted:
- id: dsh-proxy
config:
enabled: true # ≡ mode: manual
proxy: http://127.0.0.1:7890
Every save re-routes immediately. The plugin logs each switch:
dsh-proxy: routing global fetch via socks5://***@127.0.0.1:1080, noProxy 3 rule(s)
dsh-proxy: following system proxy (http://127.0.0.1:7890, noProxy 3 rule(s))
dsh-proxy: direct (mode: direct)
(Userinfo in the proxy URL is redacted in logs. system mode follows the
ambient env/OS proxy, so it never writes those env vars itself.)
What is covered / not covered
| Traffic | Routed? |
|---|---|
LLM providers via pi-ai (zai-coding-cn, custom openai-compatible routes, …) |
✅ |
dsh-llm-deepseek (deepseek-official) |
✅ |
web_search / web_fetch |
✅ |
| streamable-http MCP servers | ✅ |
stdio MCP servers, bash-tool subprocesses (curl, git, …) |
✅ via exported env, for processes spawned after the switch |
Loopback destinations (localhost, 127.0.0.0/8, ::1, 0.0.0.0) |
❌ direct by default; set bypassLoopback: false to proxy them |
| pi-ai Bedrock route | ⚠️ AWS SDK manages its own proxying (HTTPS_PROXY env is honored there) |
| Built-in browser host / browser downloads | ❌ separate process, configure the browser itself |
Also note: child processes already running when you flip the switch keep the env they were spawned with; undici's SOCKS5 agent is currently marked experimental upstream.
Development
npm install
npm test # unit + local e2e: HTTP proxy, SOCKS5, noProxy, hot-switch, env
node scripts/boot-probe.mjs # boots a real DSH tree and switches through Settings
The boot probe needs a DSH >= 0.1.7-rc.1 installation (it uses the
createRuntimeResolution / PluginPackages exports that older versions
lack — with dsh 0.1.5.x it fails with
TypeError: createRuntimeResolution is not a function). You don't have to
upgrade your global install: point DSH_ROOT at any matching package tree,
for example one installed into a scratch directory:
npm install --prefix /tmp/dsh-probe-root @deepseek-ai/dsh@0.1.7-rc.1
DSH_ROOT=/tmp/dsh-probe-root node scripts/boot-probe.mjs
Run both lines from this repository's root. The scratch install hoists the
dependencies to /tmp/dsh-probe-root/node_modules, so DSH_ROOT points at
the install anchor directory /tmp/dsh-probe-root itself — not at the
package directory.
Without DSH_ROOT, the probe resolves the dsh found on PATH and expects
that installation to already satisfy the version requirement.
License
MIT © tr1v3r
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.