Skip to content

Claude Code mods: compatibility ​

English | 中文

Every way a mod behaves differently through this bridge than under Claude Code, as of Claude Code 2.1.287 (mods reference, its claude-code.d.ts) and the Getting started with Claude Code mods post of 2026-10-01. A row names the Claude Code behavior, what this bridge does, and the reason or the follow-up. Anything not listed here behaves as the reference describes; a hook on an event this host never raises is named in a warning when the mod loads, and a $ call outside the served table rejects with no implementation for <namespace>.<method>.

Packaging and loading ​

Claude CodeThis bridgeReason or follow-up
A plugin directory with .claude-plugin/plugin.json and hooks/hooks.json naming one hooks module; claude --plugin-dir, marketplaces, /plugin installA mod is a plugin: defineMod({ name, version, root, userConfig, register }) wraps the hooks module and mounts through cordis.yml after the bridge; plugin.json and hooks.json are not readMods join the one composition mechanism every DSH plugin uses; the example directories keep their manifests so they also run under claude --plugin-dir
userConfig schema in plugin.json with typed defaults; options validated at loaduserConfig is the defaults object of the defineMod spec; the plugin's cordis.yml config overlays it and is validated as a record of strings, numbers, booleans, and string listsNo manifest is read
Tiers (user, project, managed) and next.to(e, tier)Every mod loads as user; tier() in a test is accepted and ignored; next.to rejectsManaged settings are not a DSH concept
Hot reload of the hooks module on edit; claude plugin validate, claude plugin test, types generation from types/index.d.tsNone of these commands; a remount re-runs register on the same evaluated module, so module-level variables keep their valuesFollow-up only on demand; the test kit's createModTestKit replaces claude plugin test inside Vitest
A hooks module may be TypeScriptA .ts hooks module loads only where the launcher transpiles (the source launch does; a built install does not)DSH ships plain Node
Mods run in-process with $ as their only access to the hostThe hooks module runs in-process with Node's globals, no access rule, and the process's full authority: $.env reads and writes the harness environment, $.http.fetch reaches any URL, $.fs and $.tool.call act as the sessionNo sandbox is applied to mods; mount only mods you would run as a plugin
import type { … } from 'claude-code', declare module 'claude-code' { interface PluginState }The claude-code module name resolves only inside this repository's test setup, to the bridge's types; a mod outside it imports @deepseek-ai/dsh-experimental-claude-code-modsThe type names are the bridge's own
settings hooks in hooks.json run beside mod hooksNot run; mount @deepseek-ai/dsh-hooks-claude-code for themDifferent bridge

Events ​

EventClaude CodeThis bridge
session.startRaised once per session, before the first promptRaised for a root agent from agent/created, awaited before its first turn; not raised for subagents
session.endRaised once per sessionRaised for a root agent from agent/disposed; $.state is readable until the hooks settle
prompt.submite.text is the typed prompt; context entries become one block each after the prompt as typed; a prompt a plugin submitted carries origin: { kind: 'plugin', name }Same; e.text joins the text blocks of the human's own (user-sourced) messages in the claimed batch, a batch of injected context alone raises nothing, and a prompt $.prompt.submit queued carries the submitting mod as its origin
turn.start, turn.completePer turn; subagent turns carry agentIdSame; turn.complete's { text } reaches the host log, not a line under the answer; durationMs counts from turn/start
tool.callRuns before Claude Code's permission check; next({ ...e, command }) rewrites the arguments the tool runs with, and next({ ...e, tool }) reroutes the callRuns around tools/execute, after the harness permission decision; a hook (or .catch handler) that rewrites arguments or names another tool is skipped with a report — the argument case names the pre-tool input rewrite proposal — because the logged call runs as logged. { deny }, { result }, observing, and rewriting the result or isError after next work as described
command.runRuns for a command the mod registeredSame
ui.renderRaised for every render site (AbovePrompt, Pane, ToolUse, AssistantMessage, Spinner, …)Raised for AbovePrompt only, once per redraw of a session's band; Pane hooks run only through the test kit's $.ui.mount({ component: 'Pane' })
tool.check, tool.describe, tool.list, tool.registerShape the tool list and permissionsNot raised; $.tool.register and $.tool.list are served as $ calls
turn.stepPer model stepNot raised
Every other reference event: the other prompt.* events, skill.prompt, attribution.text, command.describe, config.*, the other session.* events, agent.*, plugin.register, engine.create, telemetry.*, ui.resolve, ui.press, ui.input, ui.select, ui.focus, ui.scroll, ui.close, ui.messageRaised by the engineRegistered, never raised, named in a warning at load
classic.*Classic settings hooks as eventsRefused at register, like any unknown event name
<namespace>.<method> op events (fs.read, store.set, …)A later-loaded mod's $ call reaches the mods loaded before itSame; tool.call arrives through the tool pipeline instead

The chain ​

Claude CodeThis bridgeReason
A hook may call next more than once; each call runs the chain beneath againnext runs once: a second call returns the first run's resultOne tool execution per logged call
A hook that answers while its next is still runningThe run beneath is awaited before the hook's answer settles; a failure beneath after the answer is reportedThe event settles only when nothing it started is running
10 s own-time budget, 1 s for .catch, time inside next and $ calls excluded except $.clock.sleepSame, configurable (hookTimeoutMs, catchTimeoutMs)—
Not returning a result object is a failureSame; null is accepted as an answer (a surface drawn empty)ui.render with nothing to draw
A result's unknown fields are dropped and wrong types fail the hookA prompt.submit result with wrong types keeps the original text and only the string context linesLenient on the one event where partial results are common

$ members ​

NamespaceClaude CodeThis bridge
$.pluginname, root, tiername, root from the defineMod spec
$.uiresolve, invalidate, open, close, panes, focus, log, toast, status, notice, ask, blit, copy, press, input, selectresolve, invalidate, open, close, panes, log, toast, status, ask; open answers { isPlaced: false } with a reason and redraws the band; log, toast, status reach the host log; the rest reject
$.commandregister, run, listSame; run of a command nobody answers rejects with the registering mod named
$.toolregister, call, listSame; a registered tool is mcp__<plugin>__<tool>; call runs the harness pipeline and injects a tool's deferred contexts into the session
$.promptsubmit, composesubmit; the message is user-sourced, framed Message from the "<plugin>" mod: unless asUser, and the prompt.submit it raises names the mod as origin
$.sessionid, cwd, root, repo, model, turns, messages, usage, version, send, append, authorize, compact, surfacesid, cwd, root, model, turns, messages, usage, version; cwd and root both report the session workspace; usage.context.window is 0 and percent absent until the token meter knows the route's window; rateLimits is empty; version names this bridge
$.statePer session, survives hot reloadPer session, in memory; a read during ui.render subscribes the band to the slot
$.storePer plugin, durableSame, in the claude_code_mods storage domain, 4 MiB per plugin
$.clocknow, sleep, after, every; timers die with the pluginSame; a timer also dies with the session whose event scheduled it, and sleep rejects when the event is cancelled
$.fsread, write, list, exists, stat, ancestorsAll but ancestors; 4 MiB per file; stat.mtimeMs is 0
$.processrun, spawnrun only, argv without a shell, processTimeoutMs default
$.httpfetchSame, bodies up to 4 MiB
$.envget, setSame; the process environment is shared by every session and plugin
$.model, $.agent, $.config, $.settings, $.mcp, $.audio, $.telemetry, $.turnServedReject with no implementation; $.model.complete waits on a logged side-request event

Drawing ​

Claude CodeThis bridge
Box, Text, Button, Input, Spacer, and the host's own element kindsBox, Text, Button from $.ui.resolve(e); the tree is validated, and a Button's onPress stays in the host behind a per-drawing action id
Box props: flexDirection, padding*, gap, border*, width, height, overflow, scrollflexDirection, padding, paddingX, paddingY, gap, border, borderColor; the others serialize and the Web band ignores them
Text props: color, bold, dimColor, italic, underline, wrap, truncatecolor (the terminal palette names), bold, dimColor, italic, underline; the Web band wraps
Button props: label, hotkey, disabled, onPress; a bare digit arms the hotkeylabel, hotkey (shown as a hint), disabled, onPress (a click)
The band is one instance; hasSurvey, isWorking, maxRows, bodyColumns reflect the terminalOne instance per session, consulted in mod load order; bodyColumns and viewport.columns are the bridge's bandColumns (default 120), maxRows its bandRows (default 10), hasSurvey and isWorking are false; the Web band wraps instead of scrolling
The band redraws on the host's frame and when a $.state value a render read changesRedraws after session.start, when a $.state value the last drawing read is written, after $.ui.open, $.ui.close, $.ui.invalidate, after each tool.call chain and turn.complete settle, and after a press; a mod that changes module-level state at other times draws it at the next trigger. A press runs onPress without a time limit
Pane with focus, scroll, docked beside the transcriptNo pane: $.ui.open answers { isPlaced: false } so a mod falls back to the band; the test kit mounts Pane hooks directly
Hotkeys, TextInput, focus, scrolling, ui.press, ui.input, ui.selectNot served; follow-up

Testing ​

claude-code/testingThis bridge
describe, test(name, ($, on) => …), expect, mock, tierInside this repository the same module name provides them over Vitest; test infers the mod from the test file's location (<mod>/tests/*.test.ts → <mod>/index.ts) and defineModTests overrides it. Outside, createModTestKit and mock come from @deepseek-ai/dsh-experimental-claude-code-mods/testing
on stubs the engine's answers; the kit answers ui.* itselfSame; ui.open answers { isPlaced: false }, ui.invalidate and ui.close succeed; session.cwd, process.run, fs.*, store.*, env.* need a stub (mock.store, mock.env, mock.clock answer whole namespaces)
$.ui.mount({ plugin, surface, component, props }) → find, findAll, press, unmountSame, plus tree() and text(); every read renders afresh through every loaded mod's hooks; plugin and surface are accepted for source compatibility
5 s hook budget in testsSame (budgetMs)

The example mods ​

ModSourceThrough this bridge
Token WeatherThe post's module, type contract, and test, unchangedRuns as published; the test's three as any casts are dropped because the kit is typed. Draws in the band from $.session.usage through the token meter
Blast RadiusThe post's tool.call hook unchanged; classify, measure, and the trees completed to the post's descriptionHolds the command with real sleep polls, draws the report in the band (isPlaced: false), and Proceed or Cancel decides; load it before Token Weather
Replay TheaterThe post's five hooks unchanged; stepsFor, the diff, openReplay, the pane and the hint completedRecords Edit and Write calls, hints in the band after the turn, and steps through the diffs in the band because no pane is placed
diff (anthropics/claude-code, read at 52c7644)TypeScript, ~570 files, hooks command.run, prompt.submit, session.start, tool.call, ui.close, ui.focus, ui.render, ui.scrollNot runnable: needs $.settings.read, $.telemetry.*, $.session.messages over a pane, ui.focus and ui.scroll events, and a placed Pane. Assessed by reading; the repository's terms do not allow a fixture copy
agents-md (same repository)Hooks agent.spawn, prompt.context, session.start, tool.callNot runnable: prompt.context and agent.spawn are not raised and $.fs.ancestors, $.telemetry.* are not served
sec-default (same repository)Hooks tool.check, prompt.compose, settings.read, classic.*, plugin.register, …Not runnable: managed-tier policy over events this host does not raise
telemetry (same repository)Hooks engine.create, telemetry.*, session.start, session.end; calls $.session.repo, $.settings.readNot runnable: telemetry.* and engine.create are not raised and the two $ members are not served

Cordis API ​

Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — the language sides differ only in locale-specific paired document paths. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.

ctx.claudeCodeMods — ClaudeCodeMods ​

The bridge service: loaded mods, their hooks, and the harness listeners that raise their events. Mods join through ClaudeCodeMods.add, which defineMod calls when a mod plugin mounts. The Remote methods let a Client draw each session's band above the prompt and press its buttons.

ts
/**
 * Watch the band above the prompt of one session: the current drawing, then
 * every redraw, until the Client stops watching.
 * @param agent - the session's agent, resolved by the Gateway.
 * @param signal - carrier cancellation.
 * @returns the band's snapshots.
 */
@Remote({ mode: 'stream' }) watchBand(agent: Agent, signal: AbortSignal): AsyncIterable<SurfaceSnapshot>

/**
 * Press a button of the band's current drawing: runs the mod's `onPress` and redraws.
 * @param agent - the session's agent, resolved by the Gateway.
 * @param generation - the drawing the Client saw.
 * @param actionId - the button's action id in that drawing.
 * @returns the snapshot after the press.
 */
@Remote pressBand(agent: Agent, generation: number, actionId: string): Promise<SurfaceSnapshot>

/**
 * Load one mod beneath every mod loaded before it: run its `register`,
 * keep its hooks, and report which of its events this host never raises.
 * @param definition - the mod as its plugin defined it.
 * @returns the disposer that removes the mod's hooks, closes its timers, and releases its registrations.
 * @throws Error when the name is invalid or taken, or when `register` throws (Claude Code's `hooks module did not load` wording).
 */
async add(definition: ModDefinition): Promise<() => Promise<void>>

Types: Agent

Source: packages/experimental/claude-code-mods/src/index.ts

Released under the MIT License.