Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

A panel listing the helper agents Claude started in this session and what each one is doing.
Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.
For bigger tasks Claude can start helper agents (subagents) that work in parallel: one researches, another writes code, another reviews. Normally you only see their final reports. This mod shows a live board with every helper, its role and its status, so you know who is still working.
/agents and press Enter. The panel opens.Esc to go back to the prompt.| Key | What it does |
|---|---|
Esc | Leave the panel |
| Type | What it does |
|---|---|
/agents | Open the board |
Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.
| Setting | What it means | Default |
|---|---|---|
language (Language) | Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese). | en |
You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.
_claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.claude --plugin-dir path/to/agent-board
To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").
From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 4 automated tests of this mod:
node scripts/check-mod.mjs examples/agent-board
The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.
For developers: the examples table lists the hooks and API calls this mod demonstrates.
hooks/register.tsx 96 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AgentRow } from '../types'
5import { strings } from './i18n'
6
7// A live board of this session's subagents. Data source: $.agent.list(), polled every 2 s
8// (there is no push event for subagent status). Reads only: it never starts or stops an agent.
9const PANE = 'agent-board'
10const agents = atom({ plugin: 'agent-board', key: 'agents' } as const, [])
11const checkedAt = atom({ plugin: 'agent-board', key: 'checkedAt' } as const, 0)
12
13// Icon and color per status; the label comes from i18n.ts.
14const STATUS: Record<string, { icon: string; color: string }> = {
15 pending: { icon: '◌', color: 'warning' },
16 running: { icon: '◌', color: 'warning' },
17 waiting: { icon: '…', color: 'suggestion' },
18 idle: { icon: '●', color: 'subtle' },
19 completed: { icon: '✓', color: 'success' },
20 failed: { icon: '✗', color: 'error' },
21 killed: { icon: '✗', color: 'error' },
22}
23
24async function poll($: EngineInterface) {
25 try {
26 const list = await $.agent.list()
27 const rows: AgentRow[] = list.map(a => ({
28 id: a.id,
29 name: a.name ?? a.type,
30 role: a.description.slice(0, 80),
31 status: a.status,
32 }))
33 const at = await $.clock.now()
34 await update($, agents, () => rows)
35 await update($, checkedAt, () => at)
36 } catch {
37 // keep the last list
38 }
39}
40
41export const register: Register = (on, options) => {
42 const t = strings(options.language)
43 const label = (status: string) => t.status[status] ?? status
44
45 on('session.start', async ($, e, next) => {
46 await poll($)
47 $.clock.every(2000, () => poll($))
48 try {
49 await $.command.register({ name: 'agents', description: t.commandDescription, immediate: true })
50 } catch {
51 // name taken
52 }
53 return next(e)
54 })
55
56 on('command.run', { command: 'agents' }, async $ => {
57 await poll($)
58 const opened = await $.ui.open({ id: PANE, title: t.paneTitle, focus: true })
59 if (opened.isPlaced) return {}
60 // Nothing draws here (VS Code chat, claude -p): the same list as text.
61 const rows = await read($, agents)
62 return { text: rows.length === 0 ? t.noAgents : rows.map(r => `${r.name}: ${label(r.status)}`).join('\n') }
63 })
64
65 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
66 const { Box, Text } = $.ui.resolve(e)
67 const rows = await read($, agents)
68 const at = await read($, checkedAt)
69 const working = rows.filter(r => r.status === 'running' || r.status === 'pending').length
70
71 return (
72 <Box flexDirection="column" paddingX={1} gap={1}>
73 <Box justifyContent="space-between">
74 <Text bold color="claude">{t.heading}</Text>
75 <Text color={working > 0 ? 'warning' : 'success'}>{working > 0 ? t.working(working) : t.allIdle}</Text>
76 </Box>
77 <Box flexDirection="column">
78 {rows.length === 0 && <Text dimColor>{t.noAgentsYet}</Text>}
79 {rows.map(r => {
80 const s = STATUS[r.status] ?? { icon: '?', color: 'text' }
81 return (
82 <Box key={`agent-${r.id}`} gap={1}>
83 <Text color={s.color}>{s.icon}</Text>
84 <Box width={14}><Text bold wrap="truncate-end">{r.name}</Text></Box>
85 <Box width={12}><Text color={s.color}>{label(r.status)}</Text></Box>
86 <Box flexGrow={1}><Text dimColor wrap="truncate-end">{r.role}</Text></Box>
87 </Box>
88 )
89 })}
90 </Box>
91 <Text dimColor>{at === 0 ? t.refresh : t.refreshLast(new Date(at).toLocaleTimeString(t.locale))}</Text>
92 </Box>
93 )
94 })
95}
96hooks/i18n.ts 56 lines1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7 locale: 'en-US',
8 status: {
9 pending: 'starting',
10 running: 'working',
11 waiting: 'waiting',
12 idle: 'idle',
13 completed: 'done',
14 failed: 'failed',
15 killed: 'stopped',
16 } as Record<string, string>,
17 commandDescription: 'Show the agents of this session and the status of each',
18 paneTitle: 'Agents',
19 heading: '◆ Agents',
20 working: (n: number) => `◌ ${n} working`,
21 allIdle: '● all idle',
22 noAgents: 'No agents in this session.',
23 noAgentsYet: 'No agents in this session yet.',
24 refresh: 'refreshes every 2 s',
25 refreshLast: (time: string) => `refreshes every 2 s · last ${time}`,
26}
27
28const ptBR: typeof en = {
29 locale: 'pt-BR',
30 status: {
31 pending: 'iniciando',
32 running: 'trabalhando',
33 waiting: 'aguardando',
34 idle: 'parado',
35 completed: 'concluído',
36 failed: 'falhou',
37 killed: 'interrompido',
38 },
39 commandDescription: 'Mostra os agentes desta sessão e o status de cada um',
40 paneTitle: 'Agentes',
41 heading: '◆ Agentes',
42 working: (n: number) => `◌ ${n} trabalhando`,
43 allIdle: '● tudo parado',
44 noAgents: 'Nenhum agente nesta sessão.',
45 noAgentsYet: 'Nenhum agente nesta sessão ainda.',
46 refresh: 'atualiza a cada 2 s',
47 refreshLast: (time: string) => `atualiza a cada 2 s · última ${time}`,
48}
49
50export type Strings = typeof en
51const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
52
53export function strings(language: unknown): Strings {
54 return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
55}
56types/index.d.ts 8 lines1export type AgentRow = { id: string; name: string; role: string; status: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'agent-board': { agents: AgentRow[]; checkedAt: number }
6 }
7}
8