Read this before changing the host or client half. Every one of these was a real incident that took the app down or hung the plugin.
DSH's bundle loader (cordis-plugin-loader's unwrapExports) unwraps a
module's default export to the bare function, silently discarding
inject and name. Services the plugin declares in inject are then never
injected, and at boot you get:
Error: cannot get property "webServer" without inject
…which takes the whole profile down (plugin tree failed to load). The
dsh-pet host bundle avoids this by having no default export. Do the same:
export const name = "dsh-video-player";
export const inject = ["webServer"];
export function apply(ctx) { /* ... */ }Declaring inject: ["@deepseek-ai/dsh-client-runtime"] without requiring that
module in the client factory leaves the plugin forever in:
dsh-video-player: pending (waiting for service: @deepseek-ai/dsh-client-runtime)
This client is self-contained (plain DOM, native <video>/<iframe>), so it
injects nothing (inject: []) and activates immediately.
dsh plugin --profile web add <local-dir> installs the package as a link:
symlink. Node resolves the host module's imports from the symlink realpath,
which sits outside the profile's node_modules — so
import { installSettingsSection } from "@deepseek-ai/dsh-settings" fails to
resolve at boot. Bridge the imports into the plugin's own node_modules/:
mkdir -p <plugin>/node_modules/@deepseek-ai
ln -s ~/.dsh/profiles/node_modules/@deepseek-ai/dsh-settings <plugin>/node_modules/@deepseek-ai/dsh-settings
ln -s ~/.dsh/profiles/web/node_modules/schemastery <plugin>/node_modules/schemastery(A proper npm install — not link:) doesn't need this, because the package
lands inside the profile's node_modules tree.
dsh web resolves every bundle name as a real installed package with a
dsh.bundle.patch, and fails loud on the first bad entry — there is no
skip-fallback. A market install that writes a name without a usable bundle
(e.g. @linxin666/dsh-web-ui-all, whose npm metadata has no bundle) makes the
container crash-loop until package.json is fixed.
Prevention: run sh ~/.dsh/dsh-guard/bundle-validate.sh --profile web (or
dsh-guard.sh check) before restarting after any plugin change; --fix
removes bad entries, reset-profile.sh restores a known-good profile.
A boot failure on the live app takes the whole container down. Test first:
dsh web --port 3099 --no-open # same profile, captured logs, no risk to :3079(dsh web does NOT accept --profile — the subcommand already implies web.)
Under Docker on a WSL2 host the kernel reports "microsoft", so DSH thinks it is
in WSL and tries wslpath/powershell.exe when you click a produced file —
spawn wslpath ENOENT. Set the patch-layer override:
# ~/.dsh/profiles/web/cordis.patch.yml
- id: api-gateway
config:
nativeOpen: falseThe UI then shows produced paths as copyable text instead of a dead button.
A drag handler on the title bar swallows mousedown on child inputs/buttons,
so they can't be clicked or typed in. Exclude interactive targets:
function onBarDown(e) {
if (e.target && e.target.closest && e.target.closest("input,button,select,textarea")) return;
/* ...start drag... */
}Installing a plugin runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network. Tool approvals do not sandbox plugin code. Check the source before you install, and try unfamiliar plugins somewhere that doesn't hold your keys.