Skip to main content

Runtime overview

Asymptote supports DeepSeek Harness — DeepSeek’s open agent harness, run as dsh — through the Claude Code hook bridge DeepSeek publishes for every dsh release, sent to beacon-hooks, plus a local poll of DeepSeek’s native session files.
--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.
dsh does not read a hooks file until something mounts a reader at it. That reader is @deepseek-ai/dsh-hooks-claude-code, which 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.
The bridge is not shipped with dsh. It is only a development dependency of the dsh CLI, so a profile has it only when it has been installed into that profile, together with @deepseek-ai/dsh-hook-protocol, at exactly the dsh version. A mount dsh cannot load does not stop it from starting: the profile boots with warning: 1 entry did not activate and runs with no hooks. Asymptote checks for a loadable bridge before it mounts one, and refuses rather than writing a mount that records nothing.

Prerequisites

Before enabling DeepSeek Harness telemetry, make sure:
  • dsh is installed and has been run at least once, so the Harness home exists.
  • The hook bridge is installed in the profiles you use, at your dsh version. See Installing the hook bridge. Session sync needs no bridge.
  • 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: $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:
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.

Installing the hook bridge

The row names the bridge by package, and dsh resolves it from each profile’s directory. Install it into every profile you run, with DeepSeek’s own plugin manager, at exactly your dsh version:
Both packages are published for every dsh release and pin their dsh peers to it exactly, so a bridge for another version fails to load the same quiet way a missing one does. Upgrading dsh means installing the bridge again at the new version. Asymptote never installs these packages itself. Before it writes the row, beacon endpoint hooks install --harness dsh checks, read-only, whether a matching bridge and protocol resolve:
  • from the dsh installation, for a release that ships the bridge;
  • from each profile under $DSH_HOME/profiles; or
  • through a row that already mounts a bridge by absolute path at Asymptote’s hooks file, in the home layer or a profile’s own layer. This is how a declarative setup installs it, for example a Nix store path. Asymptote treats it as installed, writes only the hooks file, and adds no row of its own (removing one an earlier install wrote), because a second mount would run every hook twice. A bridge mounted by path does not see the packages dsh ships, which dsh provides only to profile plugins, so its declared @deepseek-ai/* peers (dsh-agent, dsh-llm, cordis and the rest) and the protocol must resolve from where it sits, for example as node_modules links into the dsh installation. Without them dsh fails to import it with Cannot find package '@deepseek-ai/dsh-llm', and Asymptote reports the mount as peers_missing rather than installed.
Every refusal and warning gives the dsh plugin --profile <name> add … command at your version, and points at beacon endpoint dsh sync, which collects sessions from the session store with no bridge at all. The dsh version is read from the installation the dsh on PATH launches, following wrapper scripts, and from dsh --version when the installation cannot be found that way. Asymptote also reads committed native session records from $DSH_HOME/sessions through:
The local, offline poll path backfills persisted assistant text, reasoning, structured tool failures, and token usage that the hook bridge does not expose. Poll events use harness.collection_method=poll and cannot approve, deny, hold, or delay a tool call.

Reading the session store

DeepSeek Harness writes each session as session.v4.jsonl.zstd (session format v4, unchanged through 0.2.1-rc.2). Asymptote decodes each tool/call’s raw JSON arguments and joins each tool/result to its call by toolCallId. Tools a run_code program dispatches (tool/ptc-dispatch-start, tool/ptc-dispatch) each become their own invocation and result, so a nested bash call is a command.executed. Only user messages with source.kind: user become prompt.submitted; the instructions, snapshots, catalogs, relayed messages and checkpoints DSH injects are not prompts. Compaction rewrites history in place, so each call’s result is recorded once, and the summarizing model call’s tokens are recorded with raw.dsh.token_source=compaction_summary. A forked session’s inherited history (closed by session/end-seed with inherited: true) was collected under the parent, so only the fork’s own activity is recorded. Every event carries the working directory from the session header, and --workspace filters on it. Model retries that committed no message, slash commands, and team, goal, plan-mode, title, and subagent-descriptor bookkeeping are not mapped.

Live hooks and the DeepSeek Harness sandbox

In every sandbox mode except danger-full-access, live hooks cannot write Asymptote’s runtime log. DeepSeek runs hook commands inside the session sandbox, whose writable boundary is the workspace. The default read-only and common workspace-write modes cannot reach the runtime log outside that boundary.
The hook remains fail-open and warns on stderr that the event was not recorded. It must not turn a telemetry write failure into a control decision. The poll path is unaffected because beacon endpoint dsh sync runs outside the harness. beacon endpoint doctor reports dsh_hook_capture: warn when native DeepSeek sessions have run since hook installation but no live hook event was retained (see dsh_hook_capture); the harness_observed check cannot catch this, because synced poll events satisfy it. Keep the sandbox and rely on scheduled poll sync, or use danger-full-access only when the security tradeoff is acceptable for the whole session. Asymptote does not change DeepSeek’s sandbox mode.
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.

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.
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.

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. 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

Show DeepSeek Harness hook status
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. A bridge mounted by path in a profile’s own cordis.patch.yml counts as the mount, and inventory lists each profile’s patch layer. Status also reports active: the files are in place and some route loads a bridge matching the running dsh. --json carries the detail under bridge: the dsh version, each profile with the bridge and protocol versions it resolves and a status (ok, missing, protocol_missing, version_mismatch, or unverified when the dsh version is unknown), and any path mounts, which can also be peers_missing. Discovery reports an installed but inactive integration as misconfigured, and beacon endpoint doctor explains it under dsh_hook_bridge. DeepSeek Harness reports as the deepseek_harness harness with capability hooks+session_log. Live hook events carry harness.collection_method=hook; backfilled native session events carry harness.collection_method=poll. 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 hook event is the same beacon-hooks binary every other hook runtime runs.

Install or configuration support

Re-running the install rewrites the hooks file wholesale, repoints the mount in place rather than adding a second one, and installs a local Asymptote-owned skill at $DSH_HOME/skills/beacon-endpoint/SKILL.md. The skill teaches DeepSeek how to check status and run workspace-scoped sync; it does not publish or upload data. Uninstall removes Asymptote’s hooks file, patch element, and marked Asymptote skill while leaving your rows, comments, and other skills alone.

Telemetry coverage

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: 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.
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.

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, Pi, OpenHands, Kiro, and 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:
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, assistant text/reasoning from poll, 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 and OpenHands, Asymptote does preserve the verbatim payload under raw.dsh, as it does for Qwen Code and 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, native session records do not represent operator decisions, and Asymptote does not synthesize one from PreToolUse. Approval rules and dashboards will show nothing for this runtime.
  • Token usage is poll-only. The bridge builds each hook payload from a fixed base plus per-event fields, and no usage count is among them on any of the seven events. beacon endpoint dsh sync reads usage from native session records when DeepSeek persisted it.
  • Failed tools are poll-only. 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. Native session poll records failed tool results when the session store marks them. A non-zero shell exit is not a tool failure and is recorded as a command exit code.
  • 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 on hooks. The bridge always sends transcript_path as an empty string. Use beacon endpoint dsh sync for the native session-store path.
  • 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.
  • Live hooks need danger-full-access. In the default read-only and common workspace-write modes, hooks cannot write the runtime log. Use native session polling when retaining the sandbox.
  • No project scope. See the warning under 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:
If DeepSeek Harness telemetry is missing:
  • Run beacon endpoint doctor. If it reports dsh_hook_bridge: warn, dsh cannot load the bridge, or some profiles cannot: it names the dsh and bridge versions and the command that installs a matching bridge. The startup line warning: 1 entry did not activate with Cannot find package '@deepseek-ai/dsh-hooks-claude-code' (or dsh-hook-protocol) is the same problem seen from dsh. See Installing the hook bridge.
  • Run beacon endpoint doctor. If it reports dsh_hook_capture: warn, the hooks are loaded but cannot write the log, which is almost always the session sandbox. See Live hooks and the DeepSeek Harness sandbox. Hook stderr, and dsh’s own hook output, will show beacon-hooks: this event was NOT recorded.
  • 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.
  • Run beacon endpoint dsh status to see whether native session records remain pending.

beacon endpoint dsh

Sync committed sessions and inspect collection progress.

Supported agent harnesses

Return to the runtime support overview.

Agent harness integration model

See how Asymptote discovers and configures each runtime.