DeepSeek Harness Headless Mode: One-Shot Agent Tasks from the CLI and CI
Run DeepSeek Harness headless with dsh --profile headless, understand exit codes and SIGTERM behavior, and script one-shot agent tasks for CI pipelines.
dsh --profile headless "your task" runs a single agent task from the terminal, prints the final response, and exits — no Web UI, no listening port, no browser required. It's the mode to reach for in scripts, cron jobs, and CI pipelines. This guide covers the command, its exit codes, how it differs from the web profile under the hood, and how it relates to the SDKs if you outgrow shell scripting.
The command
dsh --profile headless "summarize the open pull requests in this repository"
The quoted string is a positional argument — the task text itself, not a flag. Behind the scenes, dsh creates a new persistent session, runs it to completion, prints the agent's final response to stdout, and exits.
What headless actually strips out
headless and web are both reserved profile names — first use auto-initializes them from built-in templates (web = base + web-app bundle, headless = base + headless bundle). The practical difference: the headless bundle does not mount the API proxy, HTTP host, web server, web runtime, or browser client that the web profile brings in. That means:
- No listening port at all — nothing to accidentally expose to a network.
- No Web UI to click through — you get a plain terminal exchange.
- A smaller startup footprint, since an entire layer of web-facing plugins never loads.
Everything else — tool execution, the AGENTS.md/CLAUDE.md context load (up to the 65,536-byte budget), the default workspace-write sandbox — behaves the same as it does under web.
Exit codes
| Exit code | Meaning |
|---|---|
0 | Task reached completed status |
1 | Task did not complete successfully |
130 | Process received SIGINT (Ctrl+C) |
SIGTERM — the signal an orchestrator (systemd, a process manager, a CI runner's timeout) typically sends — is treated as a normal stop request in any context and always exits 0. Plugins get up to a 5-second graceful shutdown window to finish cleanup; a second signal forces an immediate exit. This matters if you're wrapping headless runs in an orchestration layer with its own timeout logic: a SIGTERM-based timeout won't look like a task failure from the exit code alone, so check task output/state separately if you need to distinguish "we killed it" from "it finished successfully."
Scripting a headless run
A minimal shell wrapper that checks the exit code:
#!/usr/bin/env bash
set -euo pipefail
if dsh --profile headless "run the test suite and report any failures"; then
echo "Agent task completed."
else
echo "Agent task did not complete." >&2
exit 1
fi
Looping headless over a list of inputs — for example, running the same task against several repositories checked out locally:
#!/usr/bin/env bash
set -euo pipefail
for repo in ./repos/*/; do
echo "== $repo =="
(cd "$repo" && dsh --profile headless "review the diff and flag anything risky")
done
Because each invocation is a fresh, complete session (no server process to keep warm between calls), this pattern is safe to run in parallel across independent working directories if your CI runner supports it — just be mindful of API rate limits on whichever model provider you've configured.
Profile setup for headless
Like any non-reserved profile, headless is bootstrapped automatically on first use with the default bundle template. If you want a headless profile with different plugins mounted — a stripped-down toolset for CI, for instance — you can create a separate custom profile name (not headless itself) and install into it:
dsh plugin --profile ci-headless add github:owner/repo
dsh --profile ci-headless "run the linter and summarize violations"
Any profile you launch this way behaves exactly like headless in terms of I/O — no port, one task, exit and print — since that behavior comes from which bundles are mounted (or, more precisely, from not mounting the web-facing bundle), not from the specific name headless.
Model configuration still applies
Headless mode reads the same shared model configuration as any other profile — $DSH_HOME/settings.yaml and $DSH_HOME/.credentials.yaml — since provider/model config lives outside any single profile. If you haven't configured a provider yet, do that through the Web UI first (see Setting Up Your DeepSeek API Key and Models), or set credentials via environment variable ahead of a CI invocation:
export DEEPSEEK_API_KEY=your-key-here
dsh --profile headless "your task"
Combining headless with --patch and --dump-config
Two launcher-level flags are worth knowing alongside headless runs, since they apply to any profile and can be layered on top of a headless invocation:
# Apply an extra config override just for this run
dsh --profile headless --patch ./ci-overrides.yml "run the linter"
# Inspect the fully-merged config a headless run would actually use, without running anything
dsh --profile headless --dump-config
--patch and --dump-config are launcher flags, not app-specific flags, so they must appear before the task string — the first token the launcher doesn't recognize is treated as the start of the app's own arguments. --dump-config is particularly useful when debugging a CI job that behaves differently than your local headless profile: run it in both environments and diff the output to see exactly which config layer differs.
Headless vs. the SDKs
If shell scripting a dsh --profile headless call starts to feel limiting — you need structured output, programmatic control over multiple turns, or you're building a larger automation on top — the next step up is one of the SDKs, which drive dsh over a stdio JSON-RPC protocol instead of parsing terminal output:
- TypeScript SDK (
dsh-sdk-client) — for Node.js-based automation. - Python SDK (
deepseek-harness-sdk,pip install deepseek-harness-sdk) — ships a bundled runtime so you don't need a system Node.js install, though it's limited to Linux x64/arm64 and macOS 14+ (arm64).
Both talk to the same underlying protocol; headless mode is the CLI-native, zero-dependency entry point, while the SDKs are for when you need the agent embedded inside a larger program rather than invoked as a subprocess.
FAQ
Does headless mode open any network port?
No. The headless bundle explicitly does not mount the API proxy, HTTP host, web server, web runtime, or browser client — there's nothing listening at all.
What exit code should my CI check for success?
0 means the task reached completed status. Any other code (1 for an incomplete task, 130 for Ctrl+C) should be treated as failure. A SIGTERM-triggered shutdown from an external timeout also returns 0, so if you need to distinguish "we killed it" from "it actually finished," check task output/state rather than relying on the exit code alone.
Can I run multiple headless tasks in parallel?
Each dsh --profile headless invocation is an independent process and session, so running several in parallel across different working directories is generally fine mechanically — the constraint you're more likely to hit is API rate limiting on your configured model provider, not dsh itself.
Is headless mode the same as the CLI reference tools like --dump-config?
No — --dump-config/--dump-default-config and --patch are launcher-level flags that work regardless of profile, used for inspecting or overriding configuration rather than running a task. See our CLI cheat sheet for the full flag reference.
Should I use headless mode or the Python/TypeScript SDK for automation?
Headless mode is the simplest entry point — one shell command, one task, plain text output — and is a good fit for CI steps and cron jobs. Reach for an SDK when you need structured/programmatic control beyond parsing stdout: see DeepSeek Harness Python SDK for the details.
Next steps
- DeepSeek Harness CLI Cheat Sheet — every launcher flag and environment variable.
- DeepSeek Harness Python SDK — programmatic control beyond shell scripting.
- Setting Up Your DeepSeek API Key and Models — configure credentials before a headless/CI run.
- DeepSeek Harness Quickstart — if you haven't installed dsh yet.