Configure dsh: API keys, models, endpoints
Where dsh keeps its config on Linux, how to wire a DeepSeek API key or a local Ollama endpoint, and exactly what leaves your box in each mode.
Where dsh keeps its configuration
dsh (DeepSeek Harness) keeps its configuration in one directory: $DSH_HOME, which defaults to ~/.dsh. Anything you set in the Web UI is written there as plain files. Copy that directory to another server and the new box behaves like the old one.
Four paths carry everything you will touch.
~/.dsh/settings.yamlholds hand-written and UI-written settings, including your provider and model routes.~/.dsh/.credentials.yamlholds the secrets. Settings keep only a reference to a credential, so the key value itself lives in one file.~/.dsh/profiles/holds named profiles, and~/.dsh/storages/holds saved sessions.~/.dsh/cordis.patch.ymlis your own patch layer. It is applied over the built-in configuration for every profile.
DeepSeek announced the harness as an MIT-licensed developer preview on 17 August 2026, and the README states there will be compatibility-breaking changes. The field names and paths in this guide match the repository documentation as of August 2026. Check them against the docs for the version you installed before copying config out of any guide, including this one, because a preview renames things between releases.
The honest minimum to first output
dsh needs Node.js 22.19 or later on the 22 line, or 24 and above. Node 23 sits outside that range. Check the version first, because a version mismatch fails at startup and the error reads like a broken package.
node -v
npx @deepseek-ai/dsh webnpx downloads the package from the npm registry and starts the Web UI on http://127.0.0.1:3080. It binds the loopback address, which means the port is not reachable from another machine even when your firewall allows it. On a VPS, forward it over SSH instead of opening 3080 to the internet.
ssh -N -L 3080:127.0.0.1:3080 you@your-serverOpen http://127.0.0.1:3080 on your laptop, then go to Settings and Models. The DeepSeek card has one API key field. Paste the key from platform.deepseek.com and save it. The model route becomes usable immediately, with no restart, because the running server stores the credential and resolves the reference live. Reaching the dsh Web UI on a remote server covers the tunnel and the reverse proxy case, and installing DeepSeek Harness on a VPS covers the server preparation this guide assumes.
After saving, look at what the app created.
ls -la ~/.dsh
stat -c '%a %n' ~/.dsh/.credentials.yamlYou should see settings.yaml, .credentials.yaml and profiles/. If stat prints a mode other than 600, run chmod 600 ~/.dsh/.credentials.yaml. A group-readable or world-readable credentials file hands your key to every other account on the box.
For a first run with no browser, one command is enough.
npx @deepseek-ai/dsh --profile headless "summarise the files in this directory"The headless profile runs a single session and prints the final answer.
Environment variables or the config file
There are two ways to give dsh a key, and they are not interchangeable.
A catalog provider (DeepSeek, Anthropic, OpenAI, and the rest of the built-in list) takes its key through the Models page. The value goes into ~/.dsh/.credentials.yaml, and your settings hold only a reference to it. The Web UI never shows the key again after you save it.
A custom provider can name an environment variable instead, with apiKeyEnv. This is the shape the documentation gives for ~/.dsh/settings.yaml.
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY
api: openai-completions
baseURL: https://gateway.example/v1
models:
- id: legacy-chat
- id: vision-preview
input: [text, image]Add one provider through the Web UI first, then open ~/.dsh/settings.yaml and copy the structure it wrote. During a developer preview the nesting is the part most likely to change, and the file the app just wrote is always current.
apiKeyEnv is read from the environment of the dsh process, not from your login shell. A key exported in an interactive session is invisible to a systemd unit, so the same config that works when you type dsh web by hand returns MISSING_CREDENTIAL under a service. Give the unit its own file.
[Service]
EnvironmentFile=/etc/dsh/dsh.envKeep that file at mode 600, owned by the user the service runs as.
Choosing models, and the ID you cannot rename
Every configured provider appears in the model picker. Selecting a model also makes it the default for new sessions. Sessions that already exist keep the model recorded in them, so switching does not rewrite an old conversation.
The Provider ID is permanent. Requests, saved sessions, model defaults and credential references all point at it, so there is no rename button. Changing it means creating a new provider and deleting the old one. Pick a name you can live with: local-ollama rather than test2.
Models are text-only unless you declare otherwise. Add input: [text, image] to a model entry to declare image support, or set defaultInput at the route level as a fallback for models the catalog does not describe. DeepSeek's own chat-completions route is text-only and cannot be configured otherwise, so an image attached to that route is refused before anything is sent.
Point dsh at a local endpoint so your code stays on the box
Ollama serves an OpenAI-compatible API on http://127.0.0.1:11434/v1. dsh talks to any OpenAI-compatible base URL through a custom provider, so the two connect with nothing in between. Set up the model server first: self-hosting an LLM with Ollama on a VPS covers the install and the model pull.
Confirm the endpoint answers before you touch dsh.
ollama list
curl -s http://127.0.0.1:11434/v1/modelsollama list prints the exact tag of every model you pulled. Copy that string. curl returns the same models as JSON. An empty list means Ollama is running with nothing pulled. Connection refused means Ollama is not running, or not listening on 11434.
Now add the provider. Ollama requires an API key field and ignores its value, so any non-empty string works.
llm-pi-ai:
providers:
local-ollama:
apiKeyEnv: OLLAMA_API_KEY
api: openai-completions
baseURL: http://127.0.0.1:11434/v1
models:
- id: <the exact tag printed by ollama list>Export the variable where the dsh process will see it.
sudo install -d -m 700 /etc/dsh
printf 'OLLAMA_API_KEY=ollama\n' | sudo tee /etc/dsh/dsh.env
sudo chmod 600 /etc/dsh/dsh.envThree failures cover almost every attempt at this. MISSING_CREDENTIAL means dsh could not read the variable named by apiKeyEnv, so check the environment of the process, not the environment of your terminal. UNKNOWN_MODEL means the id does not match a configured model, so compare it against ollama list character for character, including the tag after the colon. A 401 while fetching available models comes from model discovery, which calls GET /models on your base URL; endpoints that do not serve that path need their models typed in by hand.
One more trap is the base URL. Leave /v1 off it and the requests land on paths Ollama does not serve, so the call comes back as a 404 and the model never runs. The suffix is part of the OpenAI-compatible surface, not decoration.
If Ollama runs on a different machine, that machine's address becomes the base URL, and your prompts then cross the network in cleartext over plain HTTP. Keep it on the same host, or put it behind TLS (transport layer security) and authentication: locking down an exposed Ollama endpoint.
What leaves the machine in each mode
With a DeepSeek key, every request goes to DeepSeek's API. That request carries your prompt, the contents of the files the agent read to answer it, the output of the commands it ran, and any tool results it chose to include. Your source code is inside that payload whenever the agent opened a file. This is how a hosted model works, and it is the reason to think about which directory you start the agent in.
With another catalog provider or a company gateway, the same payload goes to that vendor instead. The base URL tells you exactly where.
With a local endpoint, the model request goes to 127.0.0.1:11434 and stays on the box. No part of your code reaches a model vendor. Three things still cross the network. npx downloads the package from the npm registry. Any tool the agent runs can reach the internet on its own, including MCP (model context protocol) servers you connected, which running MCP servers on a VPS covers in detail. And telemetry, if you turn it on.
Telemetry is off until you opt in. DSH_TELEMETRY_MODE is the consent switch, and unset, empty or unrecognised values resolve to DISABLED. In that state dsh constructs no OpenTelemetry (OTel) provider, processor or exporter, so a fresh profile makes no telemetry network request at all. FEEDBACK_ONLY opts in to feedback-triggered session log sharing. FULL also permits launcher reporting. The session feed can export session content, tool data, prompts and workspace paths, so treat FULL as sending your work to DeepSeek.
For a hard stop that does not depend on getting the mode string right, set DSH_TELEMETRY_DISABLED=1. Any non-empty value is an authoritative opt-out, and it is read before the run starts, so project code cannot switch it back on mid-session. The default collector address is harness-telemetry.deepseeksvc.com, which is a useful name to know when you read your own firewall logs.
Verify instead of trusting the setting. With a task running, list the outbound connections the process holds.
sudo ss -tnp | grep -i nodeIn local-model mode you should see the loopback connection to 11434 and no connection to a public address. Anything else is worth identifying before you continue. What a coding agent sends home runs the same check against other harnesses and explains how to read the result.
Where secrets should not go
- Shell history.
export DEEPSEEK_API_KEY=sk-...is written to~/.bash_historyin cleartext, and it stays there long after you rotate the key. Prefix the command with a space whenHISTCONTROL=ignorespaceis set, or skip the shell and write the value straight into a file with mode 600. - Committed dotfiles. A key in
~/.bashrcor~/.zshrcis onegit addaway from a public repository if you keep dotfiles in git. Rungit grep -I -n 'sk-'in that repository before you push. settings.yaml. UseapiKeyEnvfor custom providers so the file holds a variable name instead of a secret. Config files get pasted into issue reports and support chats. Credentials files do not.- Output of
envand terminal screenshots. Anything that prints the whole environment prints the key with it. - Backups.
~/.dshis worth backing up, and.credentials.yamlinside it is a live secret. Exclude that file, or encrypt the archive.
These rules are not specific to dsh, and keeping secrets out of Compose env files covers the same problem on the container side of the same server.
Living with a developer preview
Pin the version you tested, because a preview can change a config key in a patch release and your provider then fails to load. Keep settings.yaml and cordis.patch.yml in version control, with the credentials file excluded, so you can see what changed after an upgrade.
Two flags help when a profile does not behave. --dump-default-config prints the composed default configuration without booting, and --dump-config prints the composed configuration for your profile the same way. Comparing the two shows what your patch layer actually changed, which is faster than reading the layers by hand.
dsh --profile web --dump-configWhen something breaks after an upgrade, run that first. A key that moved between releases shows up as a missing branch in the dump, and the fix is a one-line edit rather than a reinstall.
FAQ
Where does dsh store my DeepSeek API key?
In $DSH_HOME/.credentials.yaml, which is ~/.dsh/.credentials.yaml unless you set DSH_HOME yourself. The Models page writes the key there, and your settings hold only a reference to it, so the secret sits in one file. Check the mode with stat -c '%a %n' ~/.dsh/.credentials.yaml and set it to 600 if it is anything looser. A custom provider can avoid the file entirely by naming an environment variable with apiKeyEnv.
How do I make dsh use a local model instead of the DeepSeek API?
Add a custom provider whose base URL is your local OpenAI-compatible endpoint. For Ollama that is http://127.0.0.1:11434/v1, with api: openai-completions and a model id copied exactly from ollama list. Ollama requires an API key value and ignores it, so any non-empty string works. Confirm the endpoint answers with curl -s http://127.0.0.1:11434/v1/models before you edit any dsh config, because a dead endpoint and a wrong config produce similar errors.
Does dsh send my code anywhere by default?
With a hosted model, yes. Your prompt and the contents of the files the agent read are inside the API request to that vendor. With a local endpoint, that request goes to loopback and stays on the machine. Telemetry is a separate feed and it is off by default: DSH_TELEMETRY_MODE resolves to DISABLED when unset, and in that state no exporter is created. Set DSH_TELEMETRY_DISABLED=1 for an opt-out that is read before the run starts.
Why does dsh report MISSING_CREDENTIAL when my variable is set?
Because dsh reads the variable named by apiKeyEnv from its own process environment. A variable exported in your shell does not reach a systemd service, a different user's session, or a process started before you exported it. Put the value in an EnvironmentFile with mode 600 for the unit, or export it in the same shell that starts dsh. Confirm what the running process actually holds with sudo tr '\0' '\n' < /proc/$(pgrep -f dsh | head -1)/environ.
Which Node.js version does dsh need?
Node.js 22.19 or later on the 22 line, or 24 and above. Node 23 falls outside the supported range. Run node -v before anything else, because a startup failure from an unsupported runtime looks like a broken install and sends people reinstalling the package instead of the runtime.