dshkit

Running DeepSeek Harness headless (and in CI)

The headless profile runs one session, emits a result and exits — no server. How to invoke it, why exit codes matter, and how to keep a CI profile separate from your interactive one.

Updated 2026-08-133 min
Short answer

dsh --profile headless "your job" executes a single session, outputs the result and terminates. It stacks the dsh-headless bundle on dsh-base, so there is no web server. Invalid commands, configuration errors and boot failures exit nonzero, which is what makes it safe to wrap in a script.

The interactive browser agent is what gets demoed, but the headless profile is where a harness earns its keep — the point at which an agent stops being a thing you drive and becomes a thing you schedule.

The invocation

dsh --profile headless "summarise why the test suite is failing"

That runs one session, emits the result, and terminates. No server is started, no port is bound, nothing waits for input.

Architecturally it is the same base as the interactive agent with a different top layer: headless stacks dsh-headless on dsh-base, where web stacks dsh-web-app. Everything in dsh-base — model adapters, tools, persistence, sandbox and approval policy, credentials, telemetry — is present either way. You are changing the shell around the agent, not the agent.

Exit codes are the contract

This is the property that makes headless mode scriptable: invalid commands, options belonging to a different mode, configuration errors and boot failures all exit nonzero.

That guarantee is worth more than it sounds. The failure mode you fear in an automated agent is not a crash — it is a silent success that did nothing. A launcher that exits nonzero on a misplaced flag means a broken pipeline announces itself on the first run rather than the tenth.

if ! dsh --profile headless "$JOB"; then
  echo "harness run failed" >&2
  exit 1
fi

Argument order, again

The launcher parses its own flags first, and the first token it does not recognise begins the app's argument list. Put --profile before anything intended for the app:

dsh --profile headless --some-app-flag "the job"

In a shell script this matters more than interactively, because you will be interpolating variables and a stray empty expansion can shift where that boundary falls. Quote your variables.

Give CI its own profile

Do not point CI at the profile you use by hand. Profiles are cheap and isolated, and plugins install into the profile's own node_modules, so a separate profile gives you a separately-scoped plugin set for free.

The reasons compound:

  • A restricted plugin set. CI has no business holding capabilities the job does not need.
  • A stricter approval policy. Approval and sandbox policy are rows from dsh-base, so a CI profile can patch them tighter without touching your interactive setup.
  • Reproducibility. Your local profile drifts as you experiment. A CI profile whose cordis.patch.yml is version-controlled does not.
  • Blast radius. An experiment that breaks your local profile does not break the pipeline.

Set it up the same way as any other profile, then verify what it actually composes:

dsh --profile ci --dump-config

Credentials in CI

Custom providers read their key from an environment variable named by apiKeyEnv, which fits CI secret injection exactly — set the secret as an environment variable in your pipeline and reference it by name.

Catalog-provider keys stored in $DSH_HOME/.credentials.yaml are a worse fit, since that file must then exist on the runner. Prefer the custom-provider path for automation, so the credential arrives through your CI secret store and never lands on disk.

Remember that an already-running process holds the environment it started with. In a pipeline this is rarely a problem — each step starts fresh — but it does mean a secret exported in an earlier step of some CI systems will not be visible later.

What headless mode does not solve

It also does not remove the need to think about what the agent is allowed to do. Sandbox and approval policy come from dsh-base and are patchable — which cuts both ways. An automated agent with a permissive approval policy and repository write access is a deployment decision, not a convenience setting. Decide it deliberately, and put the patch that encodes it in version control where a reviewer can see it.

Frequently asked

What is the difference between the web and headless profiles?

Both stack dsh-base. The web profile adds dsh-web-app for browser capability; the headless profile adds dsh-headless for single-run execution with no server.

Does headless mode check exit codes?

Yes. Invalid commands, options belonging to another mode, configuration errors and boot failures all exit nonzero — so a misconfigured pipeline fails loudly rather than silently succeeding.

Can I use a different plugin set in CI?

Yes, and you should. Plugins install per-profile into that profile's own node_modules, so a CI profile can hold a deliberately restricted set without affecting your interactive profile.

Keep reading