Runtime overview
Beacon supports DeepSeek Harness — DeepSeek’s open agent harness, run asdsh — 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 a Beacon install here is two files: the hooks file, and one row mounting the bridge at it.Prerequisites
Before enabling DeepSeek Harness telemetry, make sure:dshis 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
dshversion. See Installing the hook bridge. Session sync needs no bridge. beacon endpoint installhas run so shared endpoint config and runtime log paths exist.dshis 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
Beacon 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 Beacon adds looks like this:
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, anddsh 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:
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.
Beacon 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
dshinstallation, 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 Beacon’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. Beacon 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
dshships, whichdshprovides only to profile plugins, so its declared@deepseek-ai/*peers (dsh-agent,dsh-llm,cordisand the rest) and the protocol must resolve from where it sits, for example asnode_moduleslinks into thedshinstallation. Without themdshfails to import it withCannot find package '@deepseek-ai/dsh-llm', and Beacon reports the mount aspeers_missingrather 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.
Beacon also reads committed native session records from $DSH_HOME/sessions through:
harness.collection_method=poll, so they are historical observation only: they cannot approve, deny, hold, or delay a tool call.
Reading the session store
DeepSeek Harness writes each session assession.v4.jsonl.zstd (session format v4, unchanged through 0.2.1-rc.2). Beacon reads it as follows:
- Tool calls. A
tool/callrecord storesargumentsas the raw JSON string the model produced. Beacon decodes it, so commands, paths, patterns and edit text reach their fields. - Tool results. A
tool/resultcarries the model-facing result message: the call id asmessage.toolCallId, the output as content blocks, and the failure flag asmessage.isError, with the failure’s name and code beside it undererror. Beacon joins each result to its call and recordscommand.executed,file.read,file.modified,mcp.tool_invoked,tool.completedortool.failed. run_codedispatches. Tools arun_codeprogram calls are logged astool/ptc-dispatch-startandtool/ptc-dispatch, keyed bysubCallId. Each becomes its own invocation and result, so a nestedbashcall is acommand.executedwith its command line.- Prompts. Every user-role message the model sees is a
user/message, and only the ones withsource.kind: userare something a person sent. Agent instructions, time and runtime snapshots, skill catalogs, messages relayed from other agents, job and subagent notices, goal continuations and compaction checkpoints are context DSH injected, and are not recorded asprompt.submitted. - Compaction. History compaction rewrites earlier messages in place (a
replacesurface operation). A pruned tool result is the same call’s result again, so Beacon records each call’s result once. The tokens of the summarizing model call (compaction/summary) are recorded astoken.usagewithraw.dsh.token_source=compaction_summary. - Forks. A forked session begins with a copy of its parent’s history, closed by a
session/end-seedrecord withinherited: true. That history was already collected under the parent, so Beacon reads it for context only and records the fork’s own activity from the marker on. - Session header. The working directory, parent session and subagent origin come from the header line, which keeps its fields at the top level rather than under
data. Every event carries the working directory, and--workspacefilters on it.
assistant/attempt, llm/retry), slash commands (command/run), team, goal, plan-mode and title bookkeeping, and subagent descriptors. They remain in DSH’s own store.
Live hooks and the DeepSeek Harness sandbox
When this happens, each hook still exits0. That is deliberate: on this hook contract, exit code 2 is a block. It would refuse the tool call, erase the prompt, or stop a turn from ending, on every event, for as long as the log stays unwritable, and a telemetry write that failed must never become a control decision. Instead, the hook prints on stderr that the event was not recorded, names the log path and the error, and points at the remedies. The explanation is printed once per hook invocation, not once per event, so it stays readable wherever dsh shows or logs hook output.
The poll half is unaffected. beacon endpoint dsh sync runs outside the harness and reads DeepSeek’s own session store, so sessions the hooks missed can still be backfilled with assistant text, tool calls, commands, file activity, and token usage. What is lost is the live path: poll events arrive after the fact, carry harness.collection_method=poll, and cannot hold or deny a tool call.
beacon endpoint doctor reports this as dsh_hook_capture: warn when DeepSeek sessions have run since the hooks were installed but no live hook event has been recorded. The regular harness_observed check cannot catch it, because synced poll events satisfy it. See dsh_hook_capture.
Your options:
- Rely on the poll path. Keep the sandbox and run
beacon endpoint dsh sync, or askdshto run it through the Beacon skill. You get everything the store holds, without live capture or the policy seam. - Run
dshindanger-full-accessmode for sessions that need live capture. This removes the sandbox for the whole session, not just for the hooks, so it is a security decision to make on its own merits. Beacon does not change the sandbox mode for you.
Beacon 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 Beacon 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.
Beacon 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
insertelement carrying Beacon’s row alongside rows it did not write.
Registered events
The bridge supports seven of Claude Code’s hook events. Beacon 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; Beacon writes explicit lower values as ceilings on a hang, not budgets.
Beacon’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
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 Beacon 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
$DSH_HOME/skills/beacon-endpoint/SKILL.md. The skill only teaches dsh how to run beacon endpoint dsh status and workspace-scoped beacon endpoint dsh sync; it does not publish or upload anything.
Uninstall removes Beacon’s hooks file, its one patch element, and the Beacon-owned skill when its marker is still present. It leaves your rows, comments, and non-Beacon 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. Beacon 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 Beacon 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. Beacon 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, sotool_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.
Beacon 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. Beacon 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
Beacon records no approval decisions for DeepSeek Harness, and does not synthesize any. The bridge supports seven events andPermissionRequest 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.
Beacon’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 setfield_truncated.
Unlike Kiro and OpenHands, Beacon 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
PermissionRequestevent, native session records do not represent operator decisions, and Beacon does not synthesize one fromPreToolUse. 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 syncreads 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.
dshhas noSessionEnd.Stopfires when a turn finishes, not when a session ends, so it is recorded astool.completed. - No transcript path on hooks. The bridge always sends
transcript_pathas the empty string. Usebeacon endpoint dsh syncfor the native session-store path. - No compaction events.
PreCompactandPostCompactare among the events the bridge does not support. - Subagent detail is thin. The bridge reports a constant
agent_typeofgeneral-purposeand 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 theread-only(default) andworkspace-writesandbox modes, hooks cannot write the runtime log, so live capture records nothing andbeacon endpoint dsh syncis the collection path. See Live hooks and the DeepSeek Harness sandbox. - No project scope. See the warning under Collection path.
- Only command hooks run. The bridge runs shell-form
commandhandlers only, which is what Beacon writes. It has no bearing on Beacon’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 whiledsh is running takes effect on the next start. The install prints that reminder.
Confirm status, then generate one event and check the log:
- Run
beacon endpoint doctor. If it reportsdsh_hook_bridge: warn,dshcannot load the bridge, or some profiles cannot: it names thedshand bridge versions and the command that installs a matching bridge. The startup linewarning: 1 entry did not activatewithCannot find package '@deepseek-ai/dsh-hooks-claude-code'(ordsh-hook-protocol) is the same problem seen fromdsh. See Installing the hook bridge. - Run
beacon endpoint doctor. If it reportsdsh_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, anddsh’s own hook output, will showbeacon-hooks: this event was NOT recorded. - Confirm both files exist.
beacon endpoint hooks status --harness dshprints 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
configPathpoints 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-configto confirm the bridge row is in the tree the profile mounts. - Check the startup log under
$DSH_HOME/logs/for ahooks-claude-codewarning. A config the bridge cannot read registers no hooks and says so only there. - Restart
dshso the updated plugin tree is loaded. - Run
beacon endpoint dsh statusto see whether native session records are pending backfill.
Related
beacon endpoint dsh
Sync committed sessions and inspect collection progress.
Agent harness integration model
See how Beacon discovers and configures each runtime.