DSH plugin hooks: a tools/pre-execute permission gate
A DeepSeek Harness (DSH) plugin hook is an ordinary Cordis plugin that runs at an interception point: it listens to tools/pre-execute, receives the pending tool call, returns { kind: 'deny', reason } to deny, or returns next() to allow. A permission gate is only one example; sandbox, permission, and plan-mode plugins reuse the same extension point. It needs no external protocol, which the docs call a native hook.
What a DSH plugin hook is: an ordinary Cordis plugin at an interception point
A hook plugin is not the same thing as a permission gate — it is an ordinary Cordis plugin that runs at an interception point, and DSH plugins can intercept several extension points (source). The minimal official example is a permission gate that returns a typed decision from tools/pre-execute to allow or deny a call:
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise<boolean>
export const name = 'permission-gate'
export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
if (!(await isAllowed(exec))) {
return { kind: 'deny', reason: 'Denied by policy.' }
}
return next()
})
}
It relies on a waterfall event: tools/pre-execute is a reorderable policy layer, each listener receives exec and next(), and it can return a decision or call next() to hand off downstream. This is the same rule as the "you must call next()" discipline in the DSH plugin event system.
How to write a DSH plugin gate: allow and deny at tools/pre-execute
On tools/pre-execute, returning { kind: 'deny', reason } denies that call, and calling next() allows it or forwards it downstream (source). Write a minimal gate in three steps:
- Declare the hook plugin — export
nameat module top level, for examplepermission-gate. Expect: it takes effect as an independent plugin once loaded. - Register the listener inside
apply—ctx.on('tools/pre-execute', async (exec, next) => { ... }). Expect: your handler runs before every tool execution. - Return a decision — return
{ kind: 'deny', reason: '...' }when not allowed, andreturn next()when allowed. Expect: the denied call does not execute and carries the reason.
Return ask when you need user confirmation, then answer through ctx.approval. The official permission system / AskUserQuestion works this way: it returns ask from tools/pre-execute and registers a separate model-facing ask tool. To learn the values and outcomes of approval policy, see permission presets and approval.
Choosing DSH plugin interception points: pre-execute, guard, execute, result
The criterion for the four interception points is "do you need to change the result, and must it be enforced": use tools/pre-execute for a policy layer, ctx.tools.guard() for hard invariants, tools/execute to wrap dispatch, tools/post-execute to transform results, and tools/result to observe without changing anything (source). The comparison:
tools/pre-execute— a reorderable policy layer that returns allow / deny / ask. Expect: fits composable policy such as permission, sandbox, and plan-mode.ctx.tools.guard()— enforces invariants and provides a monotonic final denial. Expect: fits hard constraints that later listeners cannot overturn.tools/execute— wraps the dispatch lifecycle, useful for timeout / retry / metrics (onlyexec.signalis replaceable). Expect: fits a generic wrapper around a tool call.tools/post-execute— explicit result transforms; usetools/resultif you only want to observe the immutable final result. Expect: do not use post-execute when you are not changing the result.
A practical reminder: when a hook plugin intercepts a tool call, do not casually change the tool's own implementation — the tool contract belongs to ctx.tools.register() and defineTool, see tool plugin development. Self-check three things after writing: does the handler return a decision or next() on every branch; do hard constraints use guard(); does deny carry a diagnosable reason. To find an existing sandbox / permission plugin for comparison, search the DSH Plugin Hub.
FAQ
A DSH plugin hook is an ordinary Cordis plugin that runs at an interception point, not a separate protocol and not a built-in permission gate. The permission gate is just one example that returns a typed decision from tools/pre-execute, while a hook plugin can intercept other extension points as well.
A DSH plugin writes a gate in three steps: declare name, call ctx.on('tools/pre-execute', handler) inside apply, and let the handler return a typed decision or call next(). The official example returns { kind: 'deny', reason: 'Denied by policy.' } when isAllowed(exec) is false, and otherwise returns next() to allow.
A DSH plugin denies that call by returning { kind: 'deny', reason } from tools/pre-execute, and allows it by returning next() or continuing the chain with next(). It can also return ask and then answer through ctx.approval to require user confirmation.
A DSH plugin uses tools/pre-execute as a waterfall policy layer whose decision later listeners can reorder, while ctx.tools.guard() enforces an invariant with a monotonic, final denial. Use the former for composable policy and the latter for hard constraints that must not be overturned.
A DSH plugin intercepts tool calls at four main points: tools/pre-execute for a policy allow-or-deny, tools/execute to wrap the dispatch lifecycle, tools/post-execute for explicit result transforms, and tools/result to observe the immutable final result. Choose by whether you need to change the result and whether the constraint must be enforced.
Related Terms
- hook plugin
- A hook plugin is a DSH plugin form that intercepts tools or other extension points to apply policy; it runs at an interception point as an ordinary Cordis plugin without any external protocol, which the docs call a native hook.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/extension-cookbook
- tools/pre-execute
- tools/pre-execute is the DSH plugin execution-gate extension point; a hook that listens to it receives the pending ToolExecution and next(), and returns a typed decision to allow, deny, or ask about a single tool call. Sandbox, permission, and plan-mode plugins all use it.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/extension-cookbook
- PreToolDecision
- PreToolDecision is the decision type a DSH plugin returns from tools/pre-execute: deny rejects the call with a reason, while pairing it with next() allows the call or forwards it downstream.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/extension-cookbook
- ctx.tools.guard()
- ctx.tools.guard() is a DSH plugin tool guard for enforcing invariants, providing a monotonic final denial; use it when a constraint must not be overturned by later listeners instead of a reorderable policy layer.— https://deepseek-harness.github.io/deepseek-harness/en/reference/cookbook/extension-cookbook
Sources
- DeepSeek Harness docs - Extension cookbook· deepseek-ai