Claude Code Mods vs DSH Plugins: Bridge & Migration

Author
DeepSeekAgent.io Editorial Team
Published
Updated

DeepSeek Harness v0.2.1-alpha.1 introduces an experimental Claude Code Mods bridge. It does not turn DSH into Claude Code or provide one-click compatibility with every Mod. It tests a narrower architectural claim: a useful subset of the Claude Code Mod hook API can run as a higher-level interface over DSH Plugins.

That boundary determines whether a Mod can be wrapped directly, needs a focused rewrite, or depends on Claude Code internals that the bridge does not currently provide.

How the two extension models meet

A Claude Code Mod enters through register(on, options). It subscribes to engine events through on and calls Session, tool, filesystem, and UI capabilities through $. A native DSH extension is a Cordis Plugin composed by a Profile and connected to Harness services and event pipelines.

The bridge adapts the two with defineMod:

import { defineMod, type ModOn } from '@deepseek-ai/dsh-experimental-claude-code-mods'

declare const register: (on: ModOn, options: Readonly<Record<string, unknown>>) => void

export default defineMod({
  name: 'token-weather',
  version: '0.1.0',
  root: import.meta.dirname,
  register,
})

The wrapped Mod loads as a Cordis Plugin. Configuration comes from defineMod defaults plus cordis.yml overrides. Mount the bridge first, followed by Mods in hook-chain order.

Capabilities mapped today

Claude Code Mod capabilityDSH bridge behavior
session.start / session.endRaised for root-agent creation and disposal, not subagents
prompt.submitCan rewrite user text, append context, or drop a prompt
turn.start / turn.completeMapped onto the Agent turn lifecycle
tool.callCan observe, deny, answer directly, or rewrite a result after execution
command.runSupports Mod-registered commands
ui.renderSupports the AbovePrompt band only
$.tool / $.commandRegister, call, and list tools or commands
$.fs / $.process / $.httpBridge-backed file, process, and HTTP operations
$.state / $.storePer-Session state and durable per-plugin storage

The three official examples cover distinct layers. Token Weather reads usage and renders state. Blast Radius holds a command for risk confirmation. Replay Theater records Edit and Write calls and presents diffs. Together they demonstrate an observe–intercept–interact loop.

Why this is not full compatibility

The bridge does not implement the complete Claude Code engine. Migration is affected by several explicit differences:

  • DSH raises tool.call after its permission decision, so a Mod cannot rewrite an already logged tool name or arguments;
  • Pane, focus, scrolling, input controls, and most UI events are not implemented;
  • agent.spawn, prompt.context, tool.check, engine.create, and Telemetry events are never raised;
  • namespaces such as $.model, $.agent, $.settings, and $.telemetry are unavailable;
  • Claude Code user/project/managed tiers are treated as user;
  • a built DSH install does not transpile a .ts hooks module, so packages must ship JavaScript Node can load;
  • the bridge does not read plugin.json or hooks.json; identity and config move into defineMod and cordis.yml.

The official source review reaches the same conclusion for larger Mods: projects centered on Pane, Telemetry, agent.spawn, or managed policy do not run unchanged.

Security matters more than interface compatibility

A Mod executes inside the DSH Host process and is not protected by the Agent command sandbox. Although many operations use $, the hooks module itself still has Node process authority:

  • $.env reads and writes the Harness environment;
  • $.http.fetch reaches the network;
  • $.fs and $.tool.call act with the current Session's authority;
  • module code can use additional Node capabilities directly.

“The bridge loads it” is therefore not a security verdict. Review a Mod as local application code, not as a constrained prompt extension. Do not test an untrusted Mod in a Profile that contains production credentials.

Evaluate migration with two inventories

Start by extracting an event inventory:

Which on(event) registrations exist?
Which events does the bridge raise?
Is their ordering equivalent?
Does a hook attempt to rewrite tool arguments?

Then build a $ inventory:

Which $.namespace.method calls are used?
Does the bridge serve each one?
Are returned structures equivalent?
Does the design require Pane, Telemetry, Managed Tier, or complete message history?

A Mod centered on prompt.submit, tool.call, commands, files, and AbovePrompt UI is a good wrapping candidate. When its core behavior depends on events that never fire or namespaces the bridge does not serve, write a native DSH Plugin rather than accumulating compatibility patches.

A maintainable migration path

  1. Pin DSH 0.2.1-alpha.1; do not follow a floating alpha tag.
  2. Wrap the original register with defineMod and move identity and defaults into the spec.
  3. Mount the bridge before the Mod in cordis.yml.
  4. Replay events with createModTestKit, covering allow, deny, timeout, and error paths.
  5. Define explicit degradation for events that do not fire and $ methods that are absent.
  6. Validate real tool permissions, Session recovery, and plugin disposal in an isolated Profile.
  7. Rewrite as a native Plugin when DSH-specific adaptation becomes the majority of the code.

When a native DSH Plugin is the better choice

Prefer the native API when a project needs:

  • full Sidebar pages, configuration pages, or several Client Slots;
  • Sub-agent, Compaction, Provider, or Session Projection services;
  • tool-input handling before the permission decision;
  • precise lifecycle, HMR, and dependency ownership;
  • no shared hook module with Claude Code.

The bridge lowers the cost of validating existing hook logic; it does not replace the DSH Plugin API. Its architectural value is showing the common subset between the two systems and turning every remaining difference into an explicit engineering decision.

Related reading

References