DSH plugin hooks: a tools/pre-execute permission gate

Plugin DevelopmentPublished 2026-10-02Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnesshook plugintools/pre-executepermission gate
A DSH plugin hook runs at an interception point: return a typed decision from tools/pre-execute to deny, or next() to allow; guard() enforces invariants.

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:

ts
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:

  1. Declare the hook plugin — export name at module top level, for example permission-gate. Expect: it takes effect as an independent plugin once loaded.
  2. Register the listener inside apply — ctx.on('tools/pre-execute', async (exec, next) => { ... }). Expect: your handler runs before every tool execution.
  3. Return a decision — return { kind: 'deny', reason: '...' } when not allowed, and return 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:

  1. tools/pre-execute — a reorderable policy layer that returns allow / deny / ask. Expect: fits composable policy such as permission, sandbox, and plan-mode.
  2. ctx.tools.guard() — enforces invariants and provides a monotonic final denial. Expect: fits hard constraints that later listeners cannot overturn.
  3. tools/execute — wraps the dispatch lifecycle, useful for timeout / retry / metrics (only exec.signal is replaceable). Expect: fits a generic wrapper around a tool call.
  4. tools/post-execute — explicit result transforms; use tools/result if 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

What is a DSH plugin hook? Is it a permission gate?

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.

How does a DSH plugin write a tools/pre-execute gate?

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.

What does tools/pre-execute return to deny in a DSH plugin?

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.

How does ctx.tools.guard() differ from tools/pre-execute?

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.

What extension points intercept tool calls in a DSH plugin?

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