> ## Documentation Index
> Fetch the complete documentation index at: https://docs.asymptotelabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# DeepSeek Harness

> Asymptote support details for DeepSeek Harness (dsh) endpoint telemetry

## Runtime overview

Asymptote supports DeepSeek Harness — DeepSeek's open agent harness, run as `dsh` — through the Claude Code hook bridge DeepSeek ships in the CLI, sent to `beacon-hooks`.

```bash theme={null}
beacon endpoint hooks install --harness dsh
```

`--harness deepseek-harness` and `--harness deepseek_harness` are also accepted, and all three install the same thing.

DeepSeek Harness is composed rather than configured: every capability is a plugin, and the CLI, Web, ACP, and SDK surfaces are four compositions of one harness booting from one Harness home. There is one integration rather than one per surface, and one harness name — `deepseek_harness` — rather than a name per surface.

<Note>
  **`dsh` does not read a hooks file until something mounts a reader at it.** `@deepseek-ai/dsh-hooks-claude-code` ships as a dependency of the `dsh` CLI but is not in the default plugin tree, so there is no convention that picks a hooks file up. That is why an Asymptote install here is **two files**: the hooks file, and one row mounting the bridge at it.
</Note>

## Prerequisites

Before enabling DeepSeek Harness telemetry, make sure:

* `dsh` is installed and has been run at least once, so the Harness home exists.
* `beacon endpoint install` has run so shared endpoint config and runtime log paths exist.
* `dsh` is restarted after hook install or removal, unless your profile enables HMR — the config watcher picks the patch layer up live, and without it the plugin tree is read at startup.

## Collection path

Asymptote writes two files into the Harness home:

| File                                   | Owner               | Purpose                                                                        |
| -------------------------------------- | ------------------- | ------------------------------------------------------------------------------ |
| `$DSH_HOME/beacon-endpoint-hooks.json` | Asymptote, outright | The Claude Code hooks file — one command hook per event                        |
| `$DSH_HOME/cordis.patch.yml`           | **You**             | Your patch layer. Asymptote adds one row mounting the bridge at the file above |

`$DSH_HOME` defaults to `~/.dsh`, and replaces it when set — matching how `dsh` resolves the Harness home itself.

The row Asymptote adds looks like this:

```yaml theme={null}
# Beacon endpoint telemetry. Mounts DeepSeek's Claude Code hook bridge at
# a hooks file Beacon owns. Local only: the hooks make no network calls.
# Managed by `beacon endpoint hooks`; remove with `beacon endpoint hooks uninstall`.
- insert:
    - id: beacon-endpoint-hooks
      name: '@deepseek-ai/dsh-hooks-claude-code'
      config:
        configPath: '/home/you/.dsh/beacon-endpoint-hooks.json'
```

The home-level patch layer is applied over **every** profile and outranks each profile's own, so one install covers `dsh`, `dsh web`, the ACP server, and the SDK. `configPath` is absolute because the bridge resolves a relative path against the directory the process was launched from, which for an agent runtime is wherever you happened to be standing.

<Warning>
  **There is no project scope.** The bridge's `configPath` is process-level — read once at startup — and per-session discovery of a project-local hooks file is unimplemented in the bridge itself. A repository has nowhere to mount a hook bridge from, so `--level project` is **refused with that explanation** rather than quietly becoming a machine-wide install. DeepSeek Harness is also left out of the project-level `--all` sweep for the same reason.
</Warning>

### Asymptote edits your patch file as a document

`cordis.patch.yml` is yours. It carries your comments, your quoting, and `!!js` expressions the loader evaluates at boot — so Asymptote parses it into a YAML node tree, appends or removes **one self-contained element**, and re-encodes from the same tree. Comments, scalar style, and custom tags survive that round trip, and a test pins it.

A self-contained element rather than a row pushed into one of your existing `insert:` lists, so an uninstall removes exactly what an install added.

Asymptote **refuses** rather than rewriting:

* a patch file it cannot parse — unlike the hooks file, which it will replace, because this is where your whole plugin tree is tweaked and `dsh`'s parser is not Go's;
* a patch file that is not a list of entries;
* an `insert` element carrying Asymptote's row alongside rows it did not write.

<Warning>
  **An empty patch file does not boot `dsh`.** A present-but-empty or comments-only `cordis.patch.yml` fails startup — it is not ignored and it is not a warning. So an uninstall that would leave the file empty **removes the file** instead of truncating it. An absent patch layer is skipped cleanly.
</Warning>

### Registered events

The bridge supports seven of Claude Code's hook events. Asymptote registers all seven — there is nothing to leave out, because config for the other 23 is discarded before the bridge's parser sees it.

| Event              | Adapter subcommand | Timeout |
| ------------------ | ------------------ | ------- |
| `SessionStart`     | `session-start`    | 10 s    |
| `UserPromptSubmit` | `prompt-submit`    | 30 s    |
| `PreToolUse`       | `pre-tool`         | 10 s    |
| `PostToolUse`      | `post-tool`        | 10 s    |
| `Stop`             | `stop`             | 45 s    |
| `SubagentStart`    | `subagent-start`   | 10 s    |
| `SubagentStop`     | `subagent-stop`    | 10 s    |

Timeouts are **seconds**, matching Claude Code — the bridge reads `timeout` as `timeoutSec`. Its own default is 600 seconds, which is a very long time to hold an agent turn for a hook that finishes in milliseconds; Asymptote writes explicit lower values as ceilings on a hang, not budgets.

Asymptote's hooks carry no `matcher`. An absent matcher is the match-all form in this dialect, which is what a telemetry hook wants on all seven — and the bridge validates a written matcher at parse time and rejects the **whole config** on a bad one, so the value that needs no validating is the safer one to write.

## Discovery and status

```bash title="Show DeepSeek Harness hook status" theme={null}
beacon endpoint hooks status --harness dsh
```

Status reports **both** paths, because `installed` is the conjunction of the two files: a hooks file with nothing mounted at it is a file nothing reads, and a mount pointing at a missing hooks file is a bridge that logs a warning and registers nothing. `beacon endpoint inventory` lists both for the same reason, so a half-install is visible as a half-install.

DeepSeek Harness reports as the `deepseek_harness` harness with capability `hooks`, and every event carries `harness.collection_method=hook`. The bridge is a plugin, but it is DeepSeek's plugin, shipped and versioned with the CLI; what Asymptote contributes is command hooks, and the thing that runs on each event is the same `beacon-hooks` binary every other hook runtime runs.

## Install or configuration support

```bash theme={null}
beacon endpoint hooks install --harness dsh
beacon endpoint hooks status --harness dsh
beacon endpoint hooks uninstall --harness dsh
```

Re-running the install rewrites the hooks file wholesale and **repoints** the mount in place rather than adding a second one — so a reinstall after `$DSH_HOME` moved updates the path the bridge reads, and a repair does not end with each event registered twice.

Uninstall removes Asymptote's hooks file and its one patch element, and leaves your rows and comments alone.

## Telemetry coverage

| Area                                        | Support                                                                                                                                                                                |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Runtime discovery                           | Supported through both files in the Harness home                                                                                                                                       |
| Harness attribution                         | Supported. `dsh`, `dsh_cli`, `dsh-cli`, `dshcli`, `deepseek_harness`, `deepseek-harness`, `deepseekharness`, `deepseek-ai/dsh`, and `@deepseek-ai/dsh` normalize to `deepseek_harness` |
| Hook install, status, uninstall, and repair | Supported at user scope                                                                                                                                                                |
| Session lifecycle                           | Supported. `session.started` from `SessionStart`, and `tool.completed` from `Stop`                                                                                                     |
| Prompts                                     | Supported. `prompt.submitted` from `UserPromptSubmit`                                                                                                                                  |
| Tool use                                    | Supported for pre-tool and post-tool activity, with `gen_ai.tool.call.id` from `tool_use_id`                                                                                           |
| Commands                                    | Supported. `command.executed` with the command, its output, and its exit code                                                                                                          |
| File activity                               | Supported. `file.read` and `file.modified` with paths, operations, and diffs                                                                                                           |
| MCP activity                                | Supported. Recognized from `mcp__<server>__<tool>`, with the server and tool split out                                                                                                 |
| Subagents                                   | Supported. `subagent.started` and `subagent.stopped`                                                                                                                                   |
| Approval decisions                          | Not collected. See [Known gaps](#known-gaps)                                                                                                                                           |
| Token usage and cost                        | Not collected. See [Known gaps](#known-gaps)                                                                                                                                           |
| Failed tools                                | Not collected. See [Known gaps](#known-gaps)                                                                                                                                           |
| Local JSONL and dashboard                   | Supported                                                                                                                                                                              |
| MDM deployment                              | Supported for the endpoint agent. Hooks are installed in the logged-in user's Harness home                                                                                             |

### Tool taxonomy

`dsh` publishes a generated tool catalog with both names and full argument schemas, so classification is backed by schemas rather than inferred from names, and is exhaustive over what the catalog lists:

| Tool                                                                                                 | Recorded as                                                       |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `read`, `read_image`, `glob`, `grep`                                                                 | `file.read`                                                       |
| `write`                                                                                              | `file.modified`, operation `create`                               |
| `edit`                                                                                               | `file.modified`, operation `modify`                               |
| `str_replace_editor`                                                                                 | `file.read` or `file.modified`, per its `command` argument        |
| `bash`, `pwsh`                                                                                       | `command.executed`, with `command.output` and `command.exit_code` |
| `terminal_send`                                                                                      | `command.executed`, with the submitted `text` as the command      |
| `mcp__<server>__<tool>`                                                                              | `mcp.tool_invoked`, with `mcp.server` and `mcp.tool`              |
| `run_code`, `terminal_read`, `terminal_list`, `job_*`, `subagent`, `skill`, `workflow`, and the rest | `tool.invoked`                                                    |

Four entries in that table are there because the generic classifier every runtime shares gets them wrong. `write` and `edit` — the two most common write tools — contain none of the words it looks for. `terminal_read` and `terminal_list` contain "terminal", which it reads as shell execution; both read retained output or enumerate sessions, and neither runs anything.

### `terminal_send` names its command line `text`

`dsh` runs commands through a persistent terminal as well as through `bash`, and that tool's payload is `text`, not `command`. Asymptote reads it, so a command run through a terminal session carries a command line rather than being an empty `command.executed` invisible to every rule matching on `command.command`.

A call with `submit: false` is still read. It carries control characters or an incomplete REPL line rather than a whole command, so what is recorded is a *fragment* of shell input — worth knowing, and better than letting "send the command in two writes" be a way to run commands Asymptote does not record.

### `run_code` is a tool call, not a command

`run_code` executes a TypeScript program against the other tools and is the one tool that can ask for `danger-full-access`. Asymptote records it as `tool.invoked`, for two reasons.

A program's nested tool calls are **not invisible**: `dsh` dispatches them back through the complete guarded tool pipeline, which is the same seam the hook bridge listens on. So `await tools.bash({command: '...'})` inside a program fires its own `PreToolUse` and `PostToolUse` and is recorded as a command execution with a real command line. Classifying the outer wrapper as a command too would double-count it.

And `command.command` is a *shell* command line — every threat rule that reads it is written in shell terms. A TypeScript program in that field would match those rules on its comments and string literals while matching none of them on what it does, and it would do so *instead of* the precise nested events.

The program text is not lost: it is retained under `raw.dsh.tool_input.code`, redacted and size-limited like every other field.

### Exit codes come from the output text

The bridge flattens a tool result to text before a hook sees it, so `tool_response` is a plain string rather than an object. `dsh`'s `bash` and `pwsh` tools report a non-zero exit as `[exit code: N]` in that text, and that marker is the only place an exit code reaches a hook.

Asymptote reads the **last** marker in the output: everything above it is the command's own, and a command that prints something looking like the marker — a test asserting on one, a log line quoting one — would otherwise decide the event's exit code.

A non-zero exit is recorded as `command.executed` with `command.exit_code`, not as a tool failure. A failing test run is an ordinary outcome of a command that ran.

<Note>
  **`command` on `str_replace_editor` is not a shell command.** That tool multiplexes `view`, `create`, `str_replace`, and `insert` onto a `command` argument, and `command` is also what `bash` and `pwsh` call their command line. Asymptote guards on the tool name, so an editor operation is never recorded under `command.command` and a real shell command is never dropped. `view` is the case that matters most: same tool, same arguments as the three writes, and only that word separates looking from creating.
</Note>

## Approvals and enforcement

**Asymptote records no approval decisions for DeepSeek Harness, and does not synthesize any.**

The bridge supports seven events and `PermissionRequest` is not among them, so there is no approval decision anywhere on this runtime's hook surface. `PreToolUse` announces a call the agent is about to make, not a question anybody was asked. This is the same call already settled for [Cline](/runtimes/cline), [Pi](/runtimes/pi), [OpenHands](/runtimes/openhands), [Kiro](/runtimes/kiro), and [fx](/runtimes/vercel-fx).

Asymptote's observing hooks answer with an empty JSON object, which the bridge's decoder reads as no opinion. They deliberately do **not** answer `allow`: on this runtime that is a real pre-approval, so an observing hook answering it would be disarming your permission gate for every tool call rather than watching it.

Enforcement stays behind the optional, off-by-default policy provider seam (`BEACON_POLICY_PROVIDER`), which is inert unless it names an executable and fails open on any error. DeepSeek Harness is one of the runtimes where a provider deny takes effect, using Claude Code's deny shape — which is the shape the bridge's decoder reads:

```json theme={null}
{"hookSpecificOutput": {"hookEventName": "PreToolUse",
                        "permissionDecision": "deny",
                        "permissionDecisionReason": "..."}}
```

`hookEventName` must equal the firing event or the bridge discards the whole block; it passes the event name in as a guard precisely to stop a hook answering for an event that is not running.

## Data handling

DeepSeek Harness content is handled like every other runtime. Prompts, tool arguments, commands, command output, paths, and diffs are retained in local or customer-controlled logs, with local secret redaction and per-string limits applied before writing, and a hash and byte count on each content-bearing event. Events over the 64 KiB limit drop raw and retained content, preserve stable metadata, and set `field_truncated`.

Unlike [Kiro](/runtimes/kiro) and [OpenHands](/runtimes/openhands), Asymptote **does** preserve the verbatim payload under `raw.dsh`, as it does for [Qwen Code](/runtimes/qwen-code) and [Muse Code](/runtimes/muse-code). Non-MCP tool arguments reach no endpoint-schema field on any runtime, so on `dsh` this is where `run_code`'s program, a `glob` or `grep` pattern, a `terminal_send` `submit` flag, and `SessionStart`'s `source` survive. The cost is stated rather than hidden: on a `write` the payload repeats the file content the diff already carries. `raw` is the first thing dropped at the size ceiling, so the duplication is bounded and the diff is what survives.

## Known gaps

* **No approval decisions.** The bridge exposes no `PermissionRequest` event, and Asymptote does not synthesize one from `PreToolUse`. Approval rules and dashboards will show nothing for this runtime.
* **No token usage or cost.** The bridge builds each payload from a fixed base plus per-event fields, and no usage count is among them on any of the seven events. `dsh` does record usage in its own session log; nothing on the hook surface exposes it. DeepSeek Harness activity therefore does not appear in `beacon token-usage` reports or the dashboard token view.
* **No failed-tool events.** The bridge hands a hook the result's flattened text and nothing else, so a tool that threw is indistinguishable at the hook from one that returned. `tool.failed` is never recorded, and a diff is built from a write's arguments whether or not the write landed. A non-zero shell exit is *not* a tool failure and is recorded as such.
* **No session-end event.** `dsh` has no `SessionEnd`. `Stop` fires when a turn finishes, not when a session ends, so it is recorded as `tool.completed`.
* **No transcript path.** The bridge always sends `transcript_path` as the empty string: the persistence seam exposes no artifact path, and the default session log is Zstandard-compressed and not readable by a hook script.
* **No compaction events.** `PreCompact` and `PostCompact` are among the events the bridge does not support.
* **Subagent detail is thin.** The bridge reports a constant `agent_type` of `general-purpose` and uses the child's session id where Claude Code reports the parent's, so a subagent event cannot be joined to the session that spawned it.
* **No project scope.** See the warning under [Collection path](#collection-path).
* **Only command hooks run.** The bridge runs shell-form `command` handlers only, which is what Asymptote writes. It has no bearing on Asymptote's install and is noted because it bounds what else you can put in the same file.

## Deployment notes

The plugin tree is read at startup unless your profile enables HMR, so an install taken while `dsh` is running takes effect on the next start. The install prints that reminder.

Confirm status, then generate one event and check the log:

```bash theme={null}
beacon endpoint hooks status --harness dsh
beacon endpoint doctor --system
grep '"name":"deepseek_harness"' ~/.beacon/endpoint/logs/runtime.jsonl | tail -3
```

If DeepSeek Harness telemetry is missing:

* Confirm **both** files exist. `beacon endpoint hooks status --harness dsh` prints both paths; either one alone is inert.
* Check `$DSH_HOME`: if it is set, both files are under that directory, not `~/.dsh`.
* Confirm the mount's `configPath` points at the hooks file that is actually there. A reinstall repoints it.
* Confirm the hooks file contains commands with `--platform dsh`.
* Preview the composed configuration with `dsh --dump-config` to confirm the bridge row is in the tree the profile mounts.
* Check the startup log under `$DSH_HOME/logs/` for a `hooks-claude-code` warning. A config the bridge cannot read registers **no** hooks and says so only there.
* Restart `dsh` so the updated plugin tree is loaded.

## Related

<Columns cols={2}>
  <Card title="Supported agent harnesses" icon="list-check" href="/runtimes">
    Return to the runtime support overview.
  </Card>

  <Card title="Agent harness integration model" icon="plug" href="/runtimes/integration-model">
    See how Asymptote discovers and configures each runtime.
  </Card>
</Columns>
