DSH plugin user commands: DeepSeek Harness ctx.commands
To give users a slash command, a DSH plugin goes to a registry entirely separate from tools: the user command service provided by dsh-commands lets interactive adapters discover plugin-owned commands and execute them against the exact agent, creating no model message (source). This article covers the CommandDefinition field rules, the log-only command lifecycle, scoped shadowing, and the ctx.commands API; event dispatch modes are in DSH plugin events, and tool registration and contracts are in the tool execute contract.
CommandDefinition in a DSH plugin: what to fill in to register a user command
CommandDefinition is the plugin-owned registration definition, and the registry validates and freezes an effective definition detached from the original registered object (source). The fields:
name— a lowercase command name without the leading slash. To offer/plan, registerplan.description— a human-readable summary used in discovery UI.input(optional) — aCommandInputDescriptorthat currently carries onlyhint: a placeholder shown before the user supplies free-form input, advertised to capable clients. Command availability is decided by plugin composition — every adapter consuming the registry sees all effective definitions.recordInput(optional, defaults totrue) — whethercommand/runrecordsrawInput. When the command's domain event already owns that payload, set it tofalseto avoid duplicating the payload in the session log.handler—(invocation: CommandInvocation) => CommandResult | Promise<CommandResult>, which executes against the receiving agent without sending the command to the model.
Invocation and results in a DSH plugin user command: direct execution without the model
Cancellation is the adapter's job and the adapter passes the exact target agent; results render directly in the UI, not as a tool result or session event (source). The CommandInvocation a handler receives has four fields:
commandId— the pairing id already written to this invocation'scommand/runevent.agent— the exact agent whose UI received the command.rawInput— the text right after the registered command name, preserving the separator and trailing whitespace the adapter passed.signal— the cancellation signal owned by the dispatching UI request.
The returned CommandResult is one of two branches: { kind: 'success', text?, sourceEventSeq? } or { kind: 'error', text }. sourceEventSeq is optional and only used on success: when present it points to an earlier non-command event in the receiving session log, and command/done persists the same reference so clients can merge the command lifecycle with that domain projection without parsing text or relying on adjacent lines.
Discovery, scope, and lifecycle logging for DSH plugin user commands
Scope is layered and the lifecycle is log-only (source).
- Scope and shadowing — definitions on a plain context are global; definitions registered through a
commands-injected child of an agent context shadow globals for that agent.ctx.commands.register(definition)returns the exact effect disposer;ctx.commands.list(agent)returns name-sorted immutableCommandDescriptors (handler-free) after scoped shadowing;ctx.commands.find(agent, name)returns the shadow or the global definition. - Log-only lifecycle —
ctx.commands.execute(agent, line, signal)parses and executes a known command:command/runis appended before the handler is invoked andcommand/doneafter settlement (a thrown or aborted handler settles askind: 'error'). Both are direct log-only appends with no turn wrapping them, drained by persistence at ordinary checkpoints. - Admission and failure semantics — admission misses (syntax errors or unknown names) log nothing, because they never entered a handler; a
command/runappend failure fails the execution loudly, while acommand/doneappend failure on the handler-failure path is contained so the handler's own error stays the reported failure. - Parse view —
parseCommand()returns aParsedCommandbefore registry resolution, so a syntactically valid input may still point to a command that is not currently available. - Registry change notification —
commands/changeis an unfiltered registry notification (a global or scoped change may affect any UI view); observer failures are contained and cannot veto the registry mutation.
Three self-checks when done: is name lowercase with no leading slash; is recordInput set to false when a domain event already carries the payload; and does the handler return a CommandResult directly instead of trying to push the command into a model turn. To find community implementations of command plugins, you can search the DSH Plugin Hub; for how commands differ from tools see the feature-to-mechanism map.
FAQ
A DSH plugin registers a user command through the ctx.commands registry provided by dsh-commands: call ctx.commands.register(definition) with a CommandDefinition, and the registry validates and freezes an effective definition detached from the original object, returning the exact effect disposer for unregistering. Interactive adapters consume the registry, discover plugin-owned commands, and execute them against an exact agent.
A DSH plugin user command is not sent to the model: it executes directly against the receiving agent, so the CommandDefinition handler receives the exact target agent and signal in handler(invocation), and the result renders directly in the UI rather than as a tool result or session event, creating no model message at all. Tools that go through a model turn are a completely different path.
They are the log-only lifecycle records of a DSH plugin user command: command/run is appended before the handler is invoked and command/done after settlement (a thrown or aborted handler settles as kind: 'error'). Both are direct log-only appends — no turn wraps them, and persistence drains them at ordinary checkpoints. Admission misses (syntax errors or unknown names) log nothing, because they never entered a handler.
sourceEventSeq is an optional field on a DSH plugin success result: when present it points to an earlier non-command event in the receiving session log, and command/done persists the same reference so clients can merge the command lifecycle with that domain projection without parsing text or relying on adjacent lines. It is only used on kind: 'success' results.
DSH plugin commands resolve by scope: definitions registered on a plain context are global, while definitions registered through a commands-injected child of an agent context shadow globals for that agent. ctx.commands.list(agent) returns name-sorted immutable descriptors after scoped shadowing, and find(agent, name) returns the scoped shadow or the global definition.
Related Terms
- CommandDefinition
- CommandDefinition is the plugin-owned user command registration in DeepSeek Harness, carrying a lowercase name without the leading slash, a description, an optional input.hint, recordInput defaulting to true, and a handler that executes directly against the receiving agent; the registry validates and freezes an effective definition detached from the original object.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/commands
- CommandInvocation
- CommandInvocation is the object passed to one registered command handler, carrying the commandId already written to this invocation's command/run event, the exact agent whose UI received the command, the rawInput following the command name, and the signal owned by the dispatching UI request.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/commands
- sourceEventSeq
- sourceEventSeq is an optional field on a successful command result that points to an earlier non-command event in the receiving session log; command/done persists the same reference so clients can merge the command lifecycle with a richer domain projection without parsing text.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/commands
- commands/change
- commands/change is the unfiltered notification event of the user command registry, emitted when a command is registered or unregistered; because a global or scoped change may affect any UI view it is not filtered, and observer failures are contained and cannot veto the registry mutation.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/commands
Sources
- DeepSeek Harness official docs - User commands· deepseek-ai
- DeepSeek Harness official docs - Event system· deepseek-ai