Skip to main content
All posts
Development

Hooks and Slash Commands in DeepSeek-Harness (Reuse Claude Code Hooks)

How slash commands and hooks work in DeepSeek-Harness, the extension points hooks listen on, and bridge plugins that reuse Claude Code and Codex hooks.json.

DeepSeek-Harness has two distinct extension mechanisms that both look like "commands" from the outside: human commands (slash-prefixed, ctx.commands, never seen by the model) and hooks (plugins that listen on lifecycle extension points like agent/session-start and tools/pre-execute). Neither has its own manifest format — both are, like everything else in dsh, ordinary plugin registrations against a Context.

Two different things, one mechanism

It's easy to conflate "commands" and "hooks" because in other harnesses they're sometimes the same concept. In dsh's glossary, they're kept explicitly separate:

  • A human command is a slash-prefixed instruction (/compact, /goal) interpreted directly by ctx.commands. It never becomes a model message — it's a third category alongside "model-facing tool" and "shell command," reserved for things a human operator types into the interface, not something the agent decides to invoke.
  • A hook is a plugin that subscribes to a lifecycle extension point and runs code when that point fires — session start, before a model step, before a tool executes, or on a generic session event. Hooks don't have a UI surface of their own; they're pure interception.

Both are implemented the same way structurally: a plugin's apply(ctx) function registers against ctx. There's no separate commands.json or hooks.json schema native to dsh — that's a deliberate consequence of the "everything is a plugin" design covered in our architecture overview.

Human commands: ctx.commands

A plugin registers a slash command by adding an entry to ctx.commands. Two built-ins ship with the base bundles:

  • /compact — triggers the compaction subsystem (compaction/command-compact) to shrink conversation history.
  • /goal — the human-facing entry point into the dsh-goal plugin, which tracks a persistent completion goal attached to a session (active/paused/blocked/complete states, with a "goal round" cap).

Because commands are interpreted rather than sent to the model, they're the right place to put anything that should behave deterministically and never risk being reinterpreted or ignored by the LLM — administrative actions, not agent decisions.

Hooks: the extension points you can listen on

Hooks work by subscribing to a named extension point. As of August 2026, the documented extension points relevant to hook-style interception are:

Extension pointFires when
agent/session-startA new session begins
agent/pre-stepBefore the agent takes a model step (a model request plus any tools it triggers)
tools/pre-executeBefore a tool call actually runs — this is also where permission/sandbox plugins return allow/deny/ask decisions
session/eventA generic session event fires — the same point UI plugins listen on to render conversation nodes

A hook plugin is nothing more than code that registers a listener on one of these points inside apply(ctx). There's no separate hook-definition file to write; the listener registration is the hook, and like every other plugin registration it's automatically torn down when the plugin unloads.

import type { Context } from '@deepseek-ai/cordis'

export const name = 'session-logger-hook'

export function apply(ctx: Context) {
  ctx.on('agent/session-start', (session) => {
    console.log(`session started: ${session.id}`)
  })
}

(This example follows the same plugin shape covered in our custom tool tutorial — hooks and tools are both just ctx registrations, they just attach to different seams.)

Reusing Claude Code or Codex hooks.json

If you already have a hooks.json set up for Claude Code or Codex, you don't have to rewrite it as dsh extension-point listeners by hand. Two official bridge packages exist specifically for this:

PackageBridges
dsh-hooks-claude-codeClaude Code's hooks.json shell-hook protocol
dsh-hooks-codexCodex's equivalent hook configuration

Both translate the external tool's shell-hook protocol into calls against dsh's own extension points (agent/session-start, agent/pre-step, tools/pre-execute, session/event), so an existing hook script written for the other harness keeps running without a rewrite. This matters most if you're evaluating a move from Claude Code — see our DeepSeek-Harness vs Claude Code comparison for the fuller picture of what carries over and what doesn't.

Commands, hooks, or tools — which one should you build?

A common source of confusion when writing your first extension is picking the right mechanism for the job. Since all three end up as ordinary ctx registrations, the choice comes down to who's calling it and when:

You want to...Use
Let a human explicitly trigger a deterministic action from the interfaceA command (ctx.commands)
Let the model decide, mid-turn, to call something with argumentsA tool (ctx.tools.register())
React automatically to something happening in the session's lifecycle, without being explicitly invokedA hook, listening on an extension point
Bridge an already-written Claude Code or Codex hook scriptThe dsh-hooks-claude-code or dsh-hooks-codex bridge plugin, not a hand-written hook

The distinction between commands and tools matters most for anything security- or state-sensitive: a command is guaranteed to never be something the model chooses to invoke on its own, while a tool is defined precisely so the model can choose to call it. If you're building something like "wipe the session's todo list" or "force a compaction pass," making it a command rather than a tool removes an entire class of "the agent decided to do this unprompted" failure mode.

Why there's no separate manifest format

If you're coming from a harness where skills, commands, hooks, and MCP each have their own directory structure or manifest schema, the lack of one here can feel like a gap. It isn't — it's the direct consequence of dsh's microkernel design: every product feature, including commands and hooks, is implemented by registering against a Context object, with no parallel "feature-specific" file format layered on top. The tradeoff is that you need to understand the plugin/Context model to understand any single feature, including hooks — there's no shortcut manifest to skim instead. See the architecture guide for the fuller reasoning and the complete extension-point table.

FAQ

What's the difference between a human command and a tool?

A human command (/compact, /goal) is typed by a person and interpreted by ctx.commands — it never reaches the model. A tool is registered with ctx.tools.register() and is something the model itself decides to call during a turn. They're deliberately separate categories.

Can hooks block a tool call?

Yes, indirectly — tools/pre-execute is the extension point permission and sandbox plugins listen on to return allow/deny/ask decisions, so a hook registered there can effectively veto or gate execution.

Do I need to rewrite my Claude Code hooks for dsh?

Not if you use the official dsh-hooks-claude-code bridge plugin — it translates your existing hooks.json shell-hook protocol into dsh's extension points without a rewrite.

Is there a hooks.json file native to dsh?

No. Hooks in dsh are plugin code that subscribes to extension points, not a declarative JSON schema of their own. The bridge plugins exist precisely because dsh doesn't have a native equivalent format to translate into by hand.

Where do I find all the extension points dsh exposes?

The extension-point-to-mechanism mapping (tool, command, skill, MCP, hook, LLM adapter, UI, permissions, background jobs, subagent delegation) is documented as a single table in the cookbook; our architecture guide reproduces the relevant rows.

Next steps

If you're migrating an existing setup, read migrating from Claude Code to DeepSeek-Harness for the full hooks/MCP/config mapping. To understand why hooks share a mechanism with everything else in dsh, see DeepSeek-Harness architecture: everything is a plugin and the glossary for terms like "human command." For notification-style plugins that build on these extension points, browse Notifications & Integrations.