Agent setup compatibility

August 27, 2026 · View on GitHub

This matrix records what dsh-movein moves, what DSH already reads, and what remains manual. The original DSH source audit used 0.1.0-rc.6; package wiring and safe boot checks were rechecked on 0.1.1-rc.2. Hook enforcement limitations below track the still-open upstream reports rather than assuming configuration loading implies enforcement.

一份实测的 Claude Code 与 DeepSeek Harness 资产兼容性对照表,基于 DSH 0.1.0-rc.6 源码逐项验证。中文摘要在文末。

Claude Code to OpenCode

This route was loaded end to end with OpenCode 1.18.23 using debug config, debug skill, and debug agent.

Claude Code assetOpenCode resultWhat happens
Global ~/.claude/CLAUDE.mdLinked instruction fileLinks to ~/.config/opencode/AGENTS.md only when that target is free
Project CLAUDE.mdLinked instruction fileLinks to the project AGENTS.md only when that target is free
Project auto memory MEMORY.mdReferenced from project configKeeps the file in place, respects autoMemoryEnabled and autoMemoryDirectory, and sees later updates
Unconditional .claude/rules/*.mdReferenced from configAdded to the matching OpenCode instructions array without copying the file
Path-scoped .claude/rules/*.mdManualReported without conversion because OpenCode has no equivalent conditional path scope
SkillsNativeCurrent OpenCode reads global and project .claude/skills directly, so no duplicate is created
Slash commandsCopiedMarkdown and $ARGUMENTS stay unchanged in the matching OpenCode command directory
SubagentsConvertedDescription and prompt body become an OpenCode subagent. Claude tool permissions remain manual
Local MCPMechanical conversionCommand and args become one command array. ${VAR} becomes {env:VAR}
Remote MCPMechanical conversionURL and headers move into the target scope
PreToolUse command hooksLive plugin bridgeA generated global OpenCode plugin reads the original Claude settings at runtime. Matchers, exit code 2 blocking, structured deny results, and updatedInput are preserved
PostToolUse command hooksLive plugin bridgeThe same plugin runs checks after a successful tool call and adds blocking stderr or structured feedback to the OpenCode tool result
Other hook events and typesManualStop timing, prompt, agent, HTTP, async, and conditional if semantics are not guessed
Existing targetSkippedNo command, agent, instruction file, or MCP definition is overwritten
Plaintext secretSkippedA secret-looking MCP value is reported and not copied
JSONCPreservedComments, trailing commas, and unrelated settings remain intact
Invalid target configApply blockedA parse error blocks every write
PermissionsManualThe semantics differ, so dsh-movein does not guess
SessionsOut of scopeSession files are not read or written

Origin overview

OriginInstructionsSkills and commandsMCPOther behavior
Claude CodeProject native, global linkedSkills linked, commands and subagents convertedstdio and HTTPConfigures the mapped hook subset and deny or ask rules; hook enforcement still needs a canary
CodexGlobal AGENTS.md linkedPrompts convertedstdioApproval and sandbox policy remain with DSH
OpenCodeProject AGENTS.md native, one global file linkedSkills linked, commands and agents convertedlocal and remotePermissions and plugins remain manual

OpenCode compatibility

OpenCode paths and precedence follow the official V2 configuration documentation and V1 migration guide. JSONC parsing uses comments and trailing commas exactly as OpenCode permits.

OpenCode assetDSH compatibilityWhat happens
Global configRead and merged~/.config/opencode/opencode.json and opencode.jsonc load first
Custom configRead and mergedOPENCODE_CONFIG loads after global config
Project configRead and mergedDirect configs load from the nearest Git root toward the current directory after custom config
.opencode configRead and mergedThese configs load from the Git root toward the current directory after every direct project config
Custom directoryRead and mergedOPENCODE_CONFIG_DIR contributes config and file-based assets before project definitions
Project AGENTS.mdNative, zero workDSH already reads the same file
One global instruction fileOne symlinkLinked to $DSH_HOME/AGENTS.md only when the destination is free
Multiple, globbed, or remote instructionsManualReported without concatenation or network fetching
Skill directoriesFormat compatibleBoth skill and skills aliases are found and linked into DSH roots
Agent files and inline agentsConvertedV1 prompt, V2 system, and the description become a DSH skill
Command files and inline commandsConvertedTemplate and description become a user-invocable DSH skill
Local MCPMechanical conversionV1 direct maps and V2 mcp.servers maps become stdio command and args while string environment values remain intact
Remote MCPMechanical conversionV1 direct maps and V2 mcp.servers maps become streamable HTTP rows
Disabled or malformed MCPSkipped visiblyNo row is written and the dry run names the skipped server
{env:VAR}Runtime referenceConverted to process.env.VAR without reading the current value
{file:path}Preserved for reviewThe placeholder remains visible and no file is read
Permissions and pluginsManualReported as unsupported because DSH semantics differ
SessionsOut of scopeNo OpenCode session files are read or written
Invalid JSONCApply blockedA parse error makes the complete apply operation write nothing

Claude Code compatibility

Claude Code assetDSH compatibilityWhat actually happens
Project CLAUDE.mdNative, zero workinstructionFileCandidates defaults to ['AGENTS.md', 'CLAUDE.md'], local variants too. DSH discovers it from project root down to cwd and renders it the same system-reminder way Claude Code does
Global ~/.claude/CLAUDE.mdOne symlinkThe global slot is $DSH_HOME/AGENTS.md only, no CLAUDE.md fallback there. Link it and you are done
Skills (SKILL.md)Format compatible as isFrontmatter parses as an open object, only name and description are required, unknown keys (allowed-tools, license, ...) are ignored. But .claude/skills is NOT one of the roots, so skills have to land in one (see the table below)
Slash-invoking skillsSame UXUsers type /name, the model loads via a skill tool, same shape both sides
MCP servers (.mcp.json)Lossless mechanical conversionOne dsh-mcp-client config row per server (stdio and streamable-http). Tool names are literally identical, mcp__server__tool on both sides, so nothing referencing them breaks
Hooks (settings.json hooks)First party bridge, partial and not enforcement-equivalent@deepseek-ai/dsh-hooks-claude-code reads the configured file, substitutes ${CLAUDE_PROJECT_DIR}, and maps only 7 of 30 events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStart, and SubagentStop. Other events are silently skipped during parsing; only command handlers run. Matchers test DSH's lowercase tool names, so a Claude matcher such as Bash can select nothing (#582). On Windows, PowerShell may not propagate a native interpreter's exit 2, making an intended deny fail open (#2485, #3714). On every platform, {"continue":false} is recorded but does not halt the run (#1514). Loading and composing this row therefore does not prove enforcement
Permission rulesNot native, bridgeableDSH has three coarse presets and no per tool allowlist. deny/ask rules can be enforced at the tools/pre-execute gate (dsh-movein-permissions), allow rules have no equivalent, the DSH preset governs the default
Subagents (.claude/agents/*.md)No direct importDSH agent presets are directories with agent.cordis.yml, not markdown. The practical path is converting agent definitions to skills (frontmatter is nearly identical). Fun fact, DSH ships a subagent-claude-code provider that literally spawns claude as a child agent
Slash commands (.claude/commands/*.md)No file equivalentDSH commands are code-registered. User-invocable skills are the file-based substitute
SessionsHardest, avoid writing~/.dsh/sessions uses zstd-framed JSONL at SESSION_FORMAT_VERSION = 0 with an explicit no-compatibility promise and strict event invariants. Import history as plugin-sourced recall messages, never by writing session files. For conversation history use dsh-chat-import
Memory / ~/.claude miscManualNo DSH counterpart, carry what matters into AGENTS.md or skills

Verify hook enforcement after moving

dsh-movein doctor is intentionally non-invasive. It reads the three Claude settings layers, checks that every layer with supported command hooks has its own DSH bridge row and that the required packages exist, and reports definitely dead events, handler types, and matcher shapes. It does not execute a hook or mutate a settings file. doctor --live also keeps the migrated configuration boot-free: it composes that configuration, discards the bounded output, then boots only an official base/web snapshot. A green live doctor proves composition and host boot, not hook policy enforcement.

Before relying on a moved hook to block tools:

  1. Use a disposable project and a new DSH session. Add a temporary PreToolUse command hook with a lowercase DSH tool matcher (bash on POSIX, pwsh on Windows).
  2. Make the hook exit 2 and ask the agent to run a harmless command such as printing a fixed word. Confirm the tool is denied, not merely that a hook result appears in a log.
  3. On Windows, test the same interpreter shape your real hook uses. For a native command, append ; exit $LASTEXITCODE where PowerShell syntax permits it, then repeat the canary. A direct node, py, or other native child without explicit propagation can be observed as a non-blocking exit instead.
  4. Remove the temporary hook. Do not use {"continue":false} as a stop control until upstream #1514 is fixed and a runtime canary passes on the DSH version you run.

The canary is manual because automatically invoking an existing hook could execute arbitrary user commands. Migration success, --dump-config, and a bridge package being resolvable are necessary wiring checks but are not substitutes for this denial test.

Where DSH actually looks for skills

.claude/skills is not scanned, but there are six roots, not two. Read in rank order from packages/skill/skill-filesystem/src/index.ts, the first match wins on a name collision.

RankSourceRoot
100project-dsh<projectRoot>/.dsh/skills
200project-agents<projectRoot>/.agents/skills
300customwhatever customSkillDirs lists
400user-dsh$DSH_HOME/skills, default ~/.dsh/skills
500user-agents$DSH_AGENTS_HOME/skills, default ~/.agents/skills
600bundledbundledSkillDir when a deployment configures one

Three details that bite. The project root is the nearest ancestor containing .git, falling back to the cwd, so running from a subdirectory of a monorepo can silently pick a different root than you expect. The user DSH root skips its .system child. Nested discovery is not supported, a SKILL.md must sit one level under a root, not at **/SKILL.md.

dsh-movein writes into rank 100 and rank 400 (the .dsh pair) because those are DSH's own namespace, and doctor checks every root above for the silent-drop frontmatter shape, including the .agents ones you may have filled by hand.

Current verification note (2026-08-27). Migration regression coverage and the isolated live doctor target DSH 0.1.1-rc.2; the Claude-to-OpenCode path is checked against OpenCode 1.18.23. These checks validate loading, conversion, composition, and safe boot. They do not claim that the DSH hook bridge has fixed the open enforcement gaps above.

Five traps measured the hard way

  1. A patch row whose package the profile cannot resolve makes dsh web boot fatally (plugin tree failed to load), not a warning. Install first, write config rows only after the package resolves.
  2. Satellite npm dist-tags lag the core. The hooks bridge's latest was 0.0.1-rc.5 while dsh itself was 0.1.0-rc.6. Pin installs to the host dsh version.
  3. @deepseek-ai/dsh-hook-protocol is a peer the host install does not ship. Installing the hooks bridge alone still fails at boot, install the protocol package alongside.
  4. dsh plugin add never installs a release younger than 24 hours. dsh forwards installs to pnpm, and pnpm 11 ships a default supply-chain cooldown (minimumReleaseAge = 1440 minutes). A fresh dsh plugin add <pkg> silently picks the newest version older than a day and prints (x.y.z is available) for the one it skipped. Measured here: with 0.5.0 published 2h ago and 0.4.0 published 20h ago, a clean profile got 0.3.2 (30h old), reproduced independently on two machines. npx <pkg> goes through npm and gets latest immediately. If you ship a plugin, publish at least a day before you announce.
  5. A loaded hook bridge is not proof of enforcement. Unsupported events can be skipped, matchers can select no DSH tool, Windows can lose a native exit 2, and {"continue":false} is not consumed. Run the disposable deny canary above on the exact host and DSH version that will enforce policy.

npx dsh-movein automates every row of this table that can be automated, with a dry run first and a migration diff report for the rules that cannot map.

中文摘要

  • 项目 CLAUDE.md 原生兼容,DSH 默认就读,一行都不用动
  • 全局 CLAUDE.md 链接为 ~/.dsh/AGENTS.md 即可
  • SKILL.md 格式原样兼容,但 .claude/skills 不是 DSH 的默认技能根,需要落到 ~/.dsh/skills
  • .mcp.json 可无损机械转换,工具名 mcp__server__tool 两边完全一致
  • hooks 有官方桥(30 个事件映射 7 个),但加载成功不等于强制成功:Windows 原生子进程 exit 2 可能丢失,continue:false 当前不会停止运行,必须用无害 canary 实测
  • 子代理无法直接导入,转成技能最现实;会话文件格式 v0 无兼容承诺,绝对不要手写
  • 五个坑还包括:解析不到的包会让 dsh 启动直接失败、周边包 npm 标签落后于核心、hook-protocol 是宿主不带的 peer 依赖、新版本有 24 小时安装冷却、hook 配置接好仍不能证明强制生效