Defensive patterns: structured result, cleanup, credentials
Three risk surfaces (Browser-only / Host-side / Agent-facing), structured tool results, Cordis resource cleanup, and the credential three-no principles — write plugins users can actually trust.
10 min read
What you'll learn
- Distinguish the three DSH plugin risk surfaces (browser / host / agent-facing)
- Add the structured result + resource cleanup + credential handling trio to every plugin you write
- Use a "permission table" in your README to disclose scopes and credentials
Version
Compatible with: dsh 0.1.1-rc.2 (compiled from official docs/user/develop/basic/ + apps/host/)
Three risk surfaces — figure out which you are
DSH categorizes plugins by where code runs, and the defensive posture is different for each:
| Surface | Risk | Example |
|---|---|---|
| Browser-only | modifies Web UI rendering; model can't directly call internal logic | skins, themes, pets, UI enhancements |
| Host-side | runs in Node process; reads config / registers services / exposes endpoints / touches workspace | TUI, terminal, filesystem tools |
| Agent-facing | adds new tool schemas that the model can call | search, memory, shell, file ops |
Risk escalates: Browser-only tweaks DOM; Host-side reads settings; Agent-facing directly expands the model's action surface.
First step when writing a plugin: declare which class you belong to, in one line at the top of your README.
The trio: structured result + resource cleanup + credential handling
Regardless of class, you must do all three:
1. Structured result
Your tool functions must return structured data (prefer Result<T, E> shape) — never strings with error codes that force the model to parse:
type ToolResult<T> =
| { ok: true; data: T }
| { ok: false; error: { code: string; message: string; retryable: boolean } };
async function readImage(path: string): Promise<ToolResult<{ width: number; height: number }>> {
// ...
}
Models branch reliably on structured results; they frequently misread string results.
2. Resource cleanup (disposable + cleanup)
Every opened resource (file handle / db connection / child process / websocket) must attach to ctx.effect(), so Cordis auto-disposes on plugin unload:
import { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
const conn = openDatabaseConnection()
ctx.effect(() => () => conn.close()) // auto-close on unload
}
Don't manually addEventListener('unload', ...) — Cordis doesn't guarantee ordering.
3. Credential handling (never log, never inline)
- API keys / tokens only via
ctx.config.get('plugin.xxx.token'), neverconsole.logany object containing credentials - Pass debug output through
redact(obj)to mask known sensitive fields as'***' - Credential files must land with
0600permissions; never in git-tracked paths - If error stacks contain URLs with tokens, replace the token segment with
***before printing
Write a "permission table" into your README
The thing users most want to know before installing: "what does this plugin touch?" Add a table near the top:
## Permissions & credentials
| Type | Scope | Required | Source |
|---|---|---|---|
| File read | ~/Downloads | required | — |
| Network out | api.example.com | required | settings.token (user configures in Settings) |
| Child process | ffmpeg | optional (transcoding) | — |
| Shell | bash -c '<inline cmd>' | **never** | — |
Writing "never" in those cells is the strongest defensive commitment you can make — written promises beat silence.
Troubleshooting
- "plugin unloaded but resources linger": you forgot
ctx.effect(); checkdsh --profile X --dump-resourcesfor plugins still holding fds - "error stack dumps token to log": pass through
redact()beforeconsole.error - "sandbox keeps prompting": usually Host-side plugin didn't declare which commands it runs; add a sandbox whitelist to the profile
FAQ
How do I know which class my plugin belongs to?
Ask: can the model actively apply it? No = Browser-only; yes but read-only = Host-side; yes and writes = Agent-facing.
Is structured result really necessary?
Yes. Models mis-branch on string results 5-10% of the time in practice — it's a UX watershed.