Skip to main content

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:

SurfaceRiskExample
Browser-onlymodifies Web UI rendering; model can't directly call internal logicskins, themes, pets, UI enhancements
Host-sideruns in Node process; reads config / registers services / exposes endpoints / touches workspaceTUI, terminal, filesystem tools
Agent-facingadds new tool schemas that the model can callsearch, 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'), never console.log any object containing credentials
  • Pass debug output through redact(obj) to mask known sensitive fields as '***'
  • Credential files must land with 0600 permissions; 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(); check dsh --profile X --dump-resources for plugins still holding fds
  • "error stack dumps token to log": pass through redact() before console.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.