dshkit

Port a Claude Code skill to DeepSeek Harness (it already works)

dsh reads the same SKILL.md format Claude Code does. Copy the directory into ~/.agents/skills and it is discovered — verified against a running instance. The frontmatter contract, the five discovery roots, and how to confirm it loaded.

Updated 2026-08-144 min
Short answer

A Claude Code skill needs no changes. dsh parses the same SKILL.md format — required frontmatter is name (kebab-case) and description — and scans ~/.agents/skills as a shared cross-tool root. Copy the skill directory there, boot dsh, and it appears in the catalog. Unknown frontmatter keys such as version or metadata are ignored rather than rejected.

The interesting thing about DeepSeek Harness's skill system is not that it exists. It is that it reads the same SKILL.md format Claude Code does, and scans a root explicitly named for sharing between tools.

We tested it rather than assuming. A Claude Code skill, copied across unchanged, was discovered on first boot.

The test

The skill was docx-builder — a Word-document authoring skill written for Claude Code, with ClawHub-specific frontmatter in it.

mkdir -p ~/.agents/skills
cp -R ~/.claude/skills/docx-builder ~/.agents/skills/docx-builder
dsh web

That is the whole procedure. No edits to SKILL.md.

Its frontmatter carries fields dsh has never heard of:

---
name: docx-builder
description: 一套基于 docx npm 库的 Word 文档排版样式系统…
version: 1.0.1
user-invocable: true
metadata:
  openclaw:
    requires:
      bins: [node, npm]
    emoji: "📄"
---

version and the whole metadata.openclaw tree are foreign to dsh. It ignored them and kept going.

Querying the running instance:

{
  "skills": [
    {
      "name": "docx-builder",
      "description": "一套基于 docx npm 库的 Word 文档排版样式系统…",
      "modelInvocable": true
    }
  ]
}

modelInvocable: true is the part that matters — the model can reach for it on its own, not just a human invoking it explicitly.

The frontmatter contract

Frontmatter is parsed as an open YAML object. dsh interprets six keys and ignores the rest:

KeyRequiredMeaning
nameyesMust be kebab-case
descriptionyesShown in the model-facing catalog
whenToUsenoRouting hint
metadatanoFree-form
disable-model-invocationnotrue removes it from model catalogs
user-invocablenofalse removes it from human-facing commands

So the portability question reduces to: does your skill have a kebab-case name and a description? If yes, it runs.

The five discovery roots

Resolved in this rank order — nearest scope wins a duplicate name:

RankSourcePath
100project-dsh<projectRoot>/.dsh/skills
200project-agents<projectRoot>/.agents/skills
300customwhatever you put in customSkillDirs
400user-dsh$DSH_HOME/skills (default ~/.dsh/skills)
500user-agents$DSH_AGENTS_HOME/skills (default ~/.agents/skills)

Two of those five are .agents paths, not .dsh paths. That is the signal worth reading: the harness is deliberately scanning a shared agent config root rather than claiming skills as its own format.

<projectRoot> is the nearest ancestor containing .git; without one, the current cwd is used.

Layout rules

Skills are either a directory bundle or a flat file:

~/.agents/skills/
  docx-builder/
    SKILL.md          ← the skill
    references/       ← resources, do not affect the catalog
    examples/
  quick-note.md       ← flat file form, also valid

Discovery is one level deep. Only <root>/<name>/SKILL.md and <root>/<name>.md are recognised — nested **/SKILL.md is deliberately excluded. If your skill nests bundles inside bundles, flatten it.

Changes below references, scripts, assets and other bundle resources do not invalidate the catalog, so editing a reference file will not trigger a needless re-scan.

Verifying it loaded

Two ways.

The web UI lists skills once a workspace is selected.

The RPC, which is faster and scriptable. dsh exposes a JSON-RPC-ish gateway at POST /api/<method>:

curl -s -X POST http://127.0.0.1:3080/api/skill.list \
  -H 'content-type: application/json' \
  -d '{"type":"client-request","rpcId":"'"$(uuidgen)"'","method":"skill.list",
       "payload":{"sessionId":"<your-session-id>"}}'

skill.list requires a sessionId — skills resolve per session, not globally. Create one first with session.create (it takes a cwd), which is also how the project-scoped roots get resolved.

What this means if you have a skill library

If you have built skills for Claude Code, you already have dsh skills. The migration cost is a cp -R, and the same file keeps working in both tools — there is no fork, no dual-maintenance.

That is a much cheaper way into this ecosystem than writing plugins. A plugin means TypeScript, a Cordis seam, peerDependencies discipline and an npm release — see write your first plugin for what that involves. A skill means a markdown file you already wrote.

Known limits

Stated by the project itself, worth knowing before you port in bulk:

  • One level deep — nested skill trees and package manifests are ignored.
  • Project scope is the nearest .git — no alternate project-root marker, no monorepo subproject selection.
  • Malformed entries disappear with a warning — the model catalog gets no per-skill diagnostic, so an invalid skill and an absent one look identical from the model's side.
  • No body revision protocol — a loaded body is ordinary retained history. Later edits affect later calls but do not rewrite or announce anything.

Frequently asked

Do I have to rewrite a Claude Code skill for dsh?

No. We copied one across unchanged and it was discovered on first boot. dsh interprets name, description, whenToUse, metadata, disable-model-invocation and user-invocable; any other frontmatter key is ignored.

Where do skills go?

Five roots, in rank order: <projectRoot>/.dsh/skills, <projectRoot>/.agents/skills, any customSkillDirs you configure, $DSH_HOME/skills, and $DSH_AGENTS_HOME/skills — which defaults to ~/.agents/skills.

Why does my skill silently disappear?

Invocation policy fails closed. A camel-case spelling of disable-model-invocation or user-invocable, or a non-boolean value, drops the entire skill from discovery with a warning rather than ignoring just that field.

Are nested skills supported?

No. Discovery is one level deep — only <root>/<name>/SKILL.md and <root>/<name>.md. Nested **/SKILL.md is deliberately excluded.

Keep reading