Write a plugin
By the end of this walkthrough you will have built, installed, and verified a small plugin that adds a /greet command, and you will know how to change and publish it. You need pnpm on PATH.
An omdsh plugin is an npm package that declares dsh.bundle.patch and mounts in the same plugin tree as the shipped product. It is not a Skill file, not an MCP server document, and not a TypeScript file dropped into an extensions directory. You can copy examples/hello and rename it; this walkthrough builds a small greet-plugin from scratch so each file’s job is visible.
Create the package
Create a directory that is not an omdsh workspace member. Do not add it to pnpm-workspace.yaml, and do not use workspace: dependencies:
mkdir greet-plugin
cd greet-plugin
The package needs three files: package.json, cordis.patch.yml, and index.js.
Declare the bundle
package.json names the package, points at the plugin module, and exports the patch file:
{
"name": "greet-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
},
"peerDependencies": {
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
"@deepseek-ai/dsh-commands": "0.2.1-alpha.1"
}
}
Two rules keep the install safe:
- Pin
@deepseek-ai/*peers to the same DSH release omdsh ships, and keep them underpeerDependencies. Listing them underdependenciesmakesomdsh pluginreject the install, because a second Cordis or Harness copy would split the shared tree. - Keep
dsh.bundle.patchpointing at the patch file. A package without it still installs, but only as a plain library: omdsh prints a warning and adds no layer. Use that shape for a helper library that other bundles import.
Insert one plugin row
cordis.patch.yml is a YAML array of Cordis include patches, usually one insert list:
- insert:
- id: greet
name: greet-plugin
The row name must be the npm package name so Node resolves the installed module, and the row id must be unique in the composed tree. A later patch that targets an id replaces the whole config object for that id; it does not deep-merge. A patch that names a missing id is skipped silently at boot: the loader logger that reports it is not wired to stderr in the TUI host.
Register a slash command
index.js is an ordinary Cordis plugin: export name, list the host services you need in inject, and register work in apply so it is disposed automatically with the plugin:
export const name = 'greet-plugin'
export const inject = ['commands']
export function apply(ctx) {
ctx.effect(function* () {
yield ctx.commands.register({
name: 'greet',
description: 'Print a greeting from this plugin',
input: { hint: '[name]' },
handler(invocation) {
const who = invocation.rawInput.trim() || 'omdsh'
return { kind: 'success', text: `Hello, ${who}.` }
},
})
}, 'greet-plugin')
}
Command names are lowercase and have no leading slash. rawInput is the exact text after the command name.
Two boundaries keep the product coherent: ask a human only through ctx.tui.prompt after injecting tui, and store secrets through ctx.credentials. Do not take over the TTY, listen to raw terminal bytes, register a second slash-command table, or add a /settings row — those surfaces stay product-owned. User plugins lists the full compatibility contract.
Install and inspect
From the directory that contains greet-plugin:
omdsh plugin add ./greet-plugin
omdsh --dump-config
A ./path is relative to the invoking directory; if that path is missing, omdsh walks parent directories for the same relative path and fails if nothing exists, so it does not install a broken link.
Check the result in two steps:
--dump-configlistsgreet-pluginafter@agi-fans/oh-my-dshand showsid: greet.- Restart omdsh after this CLI installation, then run
/greet,/greet Ada, and/help. The new command appears under Agent Commands.
Change the plugin
- With a local path or
link:install, the Profile already points at that checkout: editindex.jsorcordis.patch.ymland restart omdsh to load the new module. - With a registry install, run
omdsh plugin update greet-pluginor add the new version, then restart.
A later version that gains dsh.bundle.patch joins the layer list on the next successful omdsh plugin run. omdsh plugin remove greet-plugin drops both the dependency and the layer. The shipped @agi-fans/oh-my-dsh layer is not a Profile dependency and is never removed.
Publish
- npm: publish the package, then install with
omdsh plugin add greet-plugin. - Tarball: ship the
pnpm packoutput, then install withomdsh plugin add ./greet-plugin-0.1.0.tgz. - Git: install with
omdsh plugin add github:<owner>/greet-plugin. A git-hosted package that builds inpreparemay need anallowBuildsentry in$OMDSH_HOME/profiles/omdsh/pnpm-workspace.yamlif pnpm blocks the script.
Import only published package exports, do not reach into refs/, and do not assume Host, HTTP, or a Web UI is mounted. User plugins defines which surfaces a bundle can rely on.