Seven defensive patterns from the DeepSeek Harness docs

Published 16 August 2026

DeepSeek Harness ships a short document of defensive patterns, each written as a general rule with the specific bug that motivated it. It is the most transferable file in the repository: none of it depends on adopting the harness, and every item corresponds to a failure mode that shows up in homegrown agent runtimes.

1. Report orthogonal outcomes independently

Never nest one flag's report inside another flag's conditional. A process can time out and exit zero, because it trapped the signal and shut down cleanly. If the timeout is only reported inside a non-zero-exit branch, that outcome disappears. Timed-out, signal, and exit code are independent facts and need independent fields.

Cost relevance: silently swallowed timeouts are how retry loops form. The caller sees success, gets nothing useful, and tries again.

2. Honour public contracts on both sides

Where an outcome has multiple valid representations, normalise them before exposing them. The worked example is model-request failures: implementations may throw or emit an error finish chunk, and the API boundary normalises everything to a terminal finish chunk. Without that, a caller catching an exception cannot tell whether it came from the provider, from middleware, from logging, or from its own code.

3. Async state is not synchronous state

Status queries are not causally tied to specific async operations. Awaiting an agent's idle state as a completion signal for one message is wrong, because multiple queued operations share execution intervals — idle may arrive before yours ran, or after someone else's did.

The prescribed fix: an automation caller owning a run defines its interval explicitly, from inbox receipt through the next whole-agent idle, and attributes outputs to that interval rather than to individual messages. Handle the nothing-to-wait-for branch too, or the caller hangs indefinitely.

An automation harness that hangs waiting for the wrong idle signal keeps a session and its children resident. The bill for that is not the hang; it is everything still alive behind it.

4. Dispose must reach quiescence, not just request it

Requesting termination is not achieving it. Dispose awaits children's actual termination before returning. And listener registries close before kill signals are sent, so late completions land silently instead of firing callbacks into a half-torn-down system.

This is the pattern with the clearest financial edge. A dispose that returns early leaves orphaned subprocesses and unclosed sandboxes running, and metered compute does not stop because its parent forgot about it.

5. Contain callback exceptions in the dispatcher

Wrap dispatch loops in try/catch. One failing subscriber must not reject the dispatch promise or starve the listeners queued behind it. In an agent runtime the starved listener is frequently the one doing accounting, which is how usage records go missing without any error surfacing.

6. Never hand untrusted output the ambient environment or predictable paths

The strongest rule in the document, and the one to adopt first. Scrub spawned command environments: remove variables matching key, secret, token, and password patterns before the subprocess starts. Use private directories created with owner-only permissions, random file names, and exclusive owner-only opens rather than predictable paths.

Two distinct threats. Environment scrubbing stops harness credentials leaking into subprocess output that the model then reads — and, once read, that output is in the session log and the context window. Random names and exclusive creation close symlink races on shared temporary directories.

7. Unlink link-shaped paths

Check whether a path is a symbolic link before unlinking, and delete the link rather than following it. Recursive removal is reserved for confirmed real directories, because it can traverse symlinks into their targets and throws on junctions. Cleanup code that deletes more than it created is a failure mode with no upper bound on the damage.

How to use this list

  1. Take patterns 1 and 4 to your subprocess and sandbox teardown code today. Those two are where money leaks.
  2. Take pattern 6 to any place you spawn a process whose output the model will read.
  3. Take pattern 3 to your automation layer, especially anywhere a wait is expressed as "until the agent is idle".
  4. Take pattern 5 to your telemetry pipeline and check that a failing listener cannot starve the accounting one.

Related


Want this applied to your own LLM spend? FinOps LLM runs a free audit of your AI costs and shows where the savings are. Book free audit →

Back to research

FAQ

Why must a process report timeout and exit code independently?

Because a process can time out and still exit zero, if it trapped the termination signal and shut down cleanly. Nesting the timeout report inside an exit-code branch makes that outcome invisible. Orthogonal outcomes need independent fields.

What does it mean that dispose must reach quiescence?

Requesting termination is not the same as achieving it. Dispose must await children's actual termination before returning, and close listener registries before sending kill signals so that late completions arrive silently rather than firing into a torn-down system.

How should an agent harness handle subprocess environments?

By scrubbing them. The documented rule is to remove variables matching key, secret, token, and password patterns before spawning, and to use private directories, random file names, and exclusive owner-only file opens rather than predictable paths.

Why is awaiting agent idle an unreliable completion signal?

Because status is a property of the whole agent, not of one message. Multiple queued operations share execution intervals, so idle can arrive before your message ran or after someone else's did. A caller must define its own interval explicitly and attribute outputs to that interval.