Building a Claude Code Mod: Hooks, State, and the Runtime Event Chain
This guide walks through building a Claude Code Mod from scratch using the Token Weather example, explaining the plugin structure, event hooks, state management, UI rendering, and the three hook patterns (observe, rewrite, answer), while comparing Claude Code's host-centric runtime control plane with DeepSeek Harness's plugin-first architecture.
Minimal Skeleton
A Claude Code Mod is a plugin with a hooks module. Requires Claude Code 2.1.287+. Mods are enabled by default. The Token Weather directory structure:
token-weather/
├── .claude-plugin/
│ ├── plugin.json
│ └── types/
├── hooks/
│ ├── hooks.json
│ └── token-weather.mjs
├── types/
│ └── index.d.ts
└── tests/
└── token-weather.test.ts .claude-plugin/plugin.jsonis the plugin manifest; hooks/hooks.json points to the module file. The module exports a register(on, options) function where handlers attach to events. Typical shape:
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
return next(e);
}); $is the Mods API (UI, session, state, files, processes, commands, model calls). e is event data (command for Bash, component/props for UI render). next(e) passes the event to the next plugin, finally to Claude Code's default behavior. This middleware model determines hook position and boundaries.
Rendering First UI
Token Weather first renders into the AbovePrompt region (above the input box). Core code:
export function register(on) {
on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
const { Box, Text } = $.ui.resolve(e);
return Box({
paddingX: 1,
children: [
Text({ color: "yellow", bold: true, children: "Clear skies" }),
],
});
});
} Boxand Text come from $.ui.resolve(e) because Claude Code may run in terminal or Desktop Code tab; different surfaces support different components. Load locally with claude --plugin-dir ./token-weather. Hot reload works on file save without session restart. Official also supports generating a mod via natural language in Claude Code, then saving the generated directory for distribution.
Reading Context Usage
Token Weather reads context window usage via $.session.usage() at two points:
on("session.start", async ($, e, next) => {
const result = await next(e);
await takeReading($);
return result;
});
on("turn.complete", async ($, e, next) => {
const result = await next(e);
if (!e.agentId) {
await takeReading($);
}
return result;
});It calls next(e) first, then reads — observing without altering original behavior. It filters out subagent turns via e.agentId to avoid mixing context readings. Because hot reload re-runs register, module-level variables reset; history is stored in $.state:
const readings = { plugin: "token-weather", key: "readings" };
async function takeReading($) {
const { context } = await $.session.usage();
if (!context?.window) return;
const tokens = context.tokens ?? 0;
const percent = context.percent ?? Math.round((tokens / context.window) * 100);
const { value: history = [] } = await $.state.get(readings);
await $.state.set(
readings,
[...history, { tokens, window: context.window, percent }].slice(-12),
);
}State persists across hot reloads. A type contract in types/index.d.ts declares the plugin's state keys; plugin.json references it. Missing declaration causes claude plugin validate to error. Validation lists hooks, calls, state reads/writes — a governance entry point. claude plugin test runs tests in real runtime; test hooks stub Claude Code responses (e.g., fixed $.session.usage()) to assert UI transitions (Clear → Showers, 67%, token delta).
Weather Bar
The resulting status bar shows current context usage, last 12 turns' growth, and previous turn delta. Three relationships surface:
Mods observe runtime without disturbing it. session.start and turn.complete let original flow complete before reading.
Mods turn invisible state into immediate feedback. Context pressure visible before compression or failure.
UI and state are bound. ui.render reads $.state; $.state.set triggers re-render automatically.
Details: component params from e.props (e.g., hasSurvey yields space, bodyColumns gives actual width). Monospace symbols (☀ ☁ ☂ ☇ ↯) preferred over emoji for terminal alignment.
Three Hook Actions
Official categorizes handler behavior: observe (Token Weather), rewrite (modify event, pass on), answer (handle fully, skip next(e)).
Blast Radius demonstrates answer : intercepts tool.call for Bash commands like rm -rf, git reset --hard, git clean, force push, DB migrations. Pauses execution, runs dry-run/checks, opens a pane for user Proceed/Cancel. Proceed → next(e); Cancel → return deny. Interception occurs before Claude Code's native behavior; UI drawn by same mod. Official notes: safety net, not permission system — can be bypassed via aliases/scripts; hard blocks belong in permission/deny rules.
Replay Theater demonstrates observe + UI : records file edits per turn. turn.start clears list, tool.call watches Edit/Write, turn.complete finalizes diffs. Registers /replay command to open a pane stepping through diffs.
Together: Token Weather makes runtime state visible; Blast Radius adds interactive checks before high-risk actions; Replay Theater turns edits into replayable evidence. All add code-driven observation, control, feedback at key Agent runtime events.
Event Chain Context
Claude Code already had extension points:
Skill — what Claude knows (task docs, processes).
MCP — what Claude can call (DB, browser, APIs).
Settings hook — shell/HTTP/prompt at lifecycle events, JSON I/O for allow/deny/log/modify.
Plugin — packaging format; can include Skill, agent, hook, MCP server, or hooks module (a Mod).
Mods run inside Claude Code: load once, retain state, draw UI, call host APIs. Enables continuous counting within a turn, state-UI binding, pausing tool calls for button input, registering slash commands bypassing model turn, interactive review panels. Mods move extension points from model periphery into the session event chain. Anthropic uses Mods internally: AGENTS.md support and /diff pane are built-in Mods (source in anthropics/claude-code/mods/).
Control Plane
Previous articles on Agent Harness (Pi/DeepSeek Harness loop control, Jev's constrained judgment, Claude.dev's rule verification) converge here. Mods open defined event facets (tool call, prompt, turn, command, UI) on a mature host. Developers place observers, policy enforcers, interaction panels. Capabilities depend on exposed events and $ API. This is a host-centric runtime control plane : core loop stays with Claude Code; external code gets defined event chain and capability set. Boundary matters: not "source-code penetration" but deeper than Skill/MCP/external hooks. A mature tool must clarify: which facets open, load order, callable capabilities, verification, disable, rollback.
Two Architectural Routes
Comparison with DeepSeek Harness ("Everything is a plugin": models, tools, skills, sessions, sandboxes, storage, loops, orchestration, UI as plugins). Cui Tianyi (via screenshot) notes directional similarity but different openness scope. DeepSeek Harness docs include a Claude Code Mods compatibility layer listing differences: tool.call fires before permission check in Claude Code, after in Harness bridge; pane rendering falls back to banners; some events register but never fire. These differences show Mods are not portable scripts — they depend on host event timing, $ APIs, UI surface availability, permission placement in event chain.
Two routes: Claude Code — mature product first; Mods open preset facets on existing host for risk alerts, context panels, edit replay. Advantage: close to use, low barrier, no full Harness needed. DeepSeek Harness — design composable runtime from bottom; model adapter, tool, session, sandbox, loop, orchestration, UI all plugins. Larger openness, but users must understand more runtime contracts and bear composition governance cost. Not about which is more advanced; one answers "how mature dev tool opens runtime control points", the other "can Agent Harness be designed plugin-first from start". Both acknowledge: Agent systems can't rely solely on prompt + tools; model-peripheral software becomes primary system capability.
Trust Boundaries
Mods run with user permissions: read/write accessible files, spawn processes, network requests, read env vars/secrets from settings, see prompts/tool calls, modify them, even approve tool calls on user's behalf. Mods are not sandboxed ; Claude Code sandbox isolates Bash commands, but Mod-spawned processes run outside. Installing a Mod is like installing a dev environment plugin, not a skin.
Personal: check hooked events and $ capabilities via claude plugin validate. Team: who publishes to marketplace, installs user/project scope, org-managed Mods only ( allowManagedModsOnly), troubleshooting with disableAllHooks or --safe-mode. Runtime control plane makes feature extension and security policy two sides of same coin.
UI rendering scope varies: terminal and Desktop Code tab support panes/bands/line replacement; VS Code extension chat panel, claude -p, Agent SDK, cloud sessions may run hooks but not display UI. Long-term Mods need fallback: transcript message or command result when pane unavailable.
Leaving Rules Behind
Token Weather (~80 lines) is a good starter; can also be generated by describing the mod to Claude Code with hot reload. But stopping at "Claude writes a plugin" treats Mods as ad-hoc hacks. Next step: a Mod that stays becomes a runtime rule.
Token Weather for team: thresholds for alerts, telemetry logging, per-project thresholds, fallback for claude -p /cloud.
Blast Radius in production: rule scope — high-risk directories, mandatory dry-run for migrations, force push on release branches, deny reason phrasing to prevent workarounds.
Replay Theater in code review: retention, redaction, linking to git diff/test results/issue numbers, evidence for postmortems.
Mods move personal habits, scripts, ad-hoc confirmations into installable, updatable, disableable, testable, auditable components. Building a Mod is low barrier; the work is deciding which runtime rules deserve to stay and on which event facet they belong. This is why architects should watch Mods: they don't open all Harness internals, but push "what to observe, what to intercept, how to feedback to humans, which policies org manages" — formerly platform-layer concerns — into daily dev tools. Capability moves inward; trust boundaries must be explicit. Mods' value and risk both sit at that position.
References
Anthropic, Mods overview (https://code.claude.com/docs/en/plugins/mods/overview)
Anthropic, Plugins overview (https://code.claude.com/docs/en/plugins/overview)
Addy Osmani, Getting started with Claude Code Mods (https://claude.dev/blog/getting-started-with-claude-code-mods/)
Anthropic, Claude Code settings (https://code.claude.com/docs/en/settings)
Anthropic, claude-code/mods README (https://github.com/anthropics/claude-code/tree/main/mods)
DeepSeek Harness, Everything is a plugin (https://dshai.net/)
DeepSeek Harness, Claude Code Mods: Compatibility (https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/claude-code-mods.zh.md)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
