One live line above the prompt per running subagent: time, tool count and what it's doing. /radar shows every agent and its messages.

Function-hook mods for Claude Code: shell safety, secret guarding, session telemetry, context panels, and workflow tools. Each mod is an isolated plugin with its own tests, and you enable only the ones you want.
| Mod | Description |
|---|---|
guarda | Intercepts risky Bash commands (rm -rf, git reset --hard, force push, new dependencies) and asks for confirmation explaining the impact. |
lixeira | Sends rm / Remove-Item targets to the Windows Recycle Bin instead of deleting them permanently. |
cofre | Keeps API keys out of the conversation: /chave stores them in .env, secret file reads display only variable names, outputs are masked, and pasted keys are quarantined. |
atomic-commit-guard | Blocks commits that mix top-level directories, violate Conventional Commits, or lack a Co-Authored-By trailer. |
universal-audit-log | A hook on * that logs every event (tool calls, prompts, turns, fs/http/process calls from other plugins) with secret redaction. |
| Mod | Description |
|---|---|
context-monitor | Context weather and token usage above the prompt, replay and activity panels, plain-language explanation before each edit, and ai-memory notes. |
cache-frio | Confirms before submitting a prompt to a large conversation whose prompt cache has gone cold. |
session-telemetry | Timestamp on each response, TTFT and tok/s per step, and visual notice whenever the responding model switches. |
session-time-machine | /timemachine: renders the session as a timeline of prompts, tool calls, and turn completions. |
turn-inspector | Side pane displaying the session turn-by-turn: every model request and what it carried. |
clean-view | Collapses each successful tool call into a single concise line (Edit file +12 −3); errors and in-flight calls retain the full block. |
webfetch-cache | Serves repeated WebFetch requests (identical URL and prompt) directly from the session cache. |
reflexo | Detects corrections and constraints in prompts, offering to save them as rules in CLAUDE.md. |
| Mod | Description |
|---|---|
agent-flow | Live diagram of agent flow: main loop context, subagent tree, and context passed/returned by each agent. |
agent-radar | One live line per running subagent above the prompt; /radar opens a pane with messages from each. |
delegation-dock | Status line indicator showing in-flight delegations (agy/codex/ollama) and alerting when one returns empty. |
phase-tracker | Tracks and displays the current development phase (Spec → Red Test → Surgical Code → Verification). |
next-prompts | Band with 3 next-step suggestions generated by a free Ollama cloud model; click one to draft. |
git-sidebar | Lazygit-style side pane: worktrees, local branches, branch switching, and worktree checkout. |
wait-quiz | Quizzes a flashcard in a band above the prompt while Claude is working. |
meus-mods | Central hub: quick-access bar above the prompt and /mods command displaying the status of each mod. |
Requires Claude Code with function-hook mods support.
git clone https://github.com/Covre81/claude-code-mods.git ~/.claude/mods
Enable the mods by listing their absolute paths in CLAUDE_CODE_PLUGIN_DIRS inside ~/.claude/settings.json. On Windows, paths are separated by ;:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "C:/Users/me/.claude/mods/guarda;C:/Users/me/.claude/mods/cofre;C:/Users/me/.claude/mods/session-telemetry"
}
}
Order matters: the last plugin in the list sits closest to the engine. For instance, lixeira should come after guarda so guarda inspects the original rm -rf first.
Several mods include a command to toggle them on or off without restarting (/webfetch-cache on|off, /radar-toggle, /telemetria…); /mods displays the status of all mods.
Each mod follows the same structure:
<mod>/
.claude-plugin/plugin.json manifest
.claude-plugin/types/ engine types (git-ignored, see below)
hooks/hooks.json hooks registered by the mod
hooks/register.ts(x) code
hooks/*.test.ts(x) tests
Engine types are not committed to the repository. To typecheck and test a mod, copy .claude-plugin/types from an existing mod or generate them with your Claude Code instance.
Tests run directly within Claude Code, not via vitest/jest:
claude plugin test guarda # single mod
for d in */hooks/hooks.json; do claude plugin test "${d%%/*}"; done # all mods
Commits follow Conventional Commits, scoped to one mod per commit (enforced by atomic-commit-guard).
Six mods (agent-flow, git-sidebar, session-time-machine, turn-inspector, universal-audit-log, webfetch-cache) originate from davila7/claude-code-templates, and agent-radar originates from hamzafer/claude-code-mods, both under the MIT License and adapted here. Copyright notices are preserved in THIRD_PARTY_NOTICES.md.
hooks/register.tsx 297 lines1// Agent Radar: what are my subagents cooking?
2// The band above the prompt has one live line per running subagent: status, time, tool
3// count and what it is doing right now; a finished one shows a check for 30 s. A toast
4// says when each finishes. /radar opens a pane with every agent and its messages.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import type { RadarAgent } from '../types'
9import { bandsFile, parseOrder, stackBands } from './bands'
10
11const PANE = 'agent-radar'
12const SHOW_DONE_MS = 30_000 // a finished agent stays in the band this long
13const BAND_ROWS = 4
14
15// Held by the host, so the radar survives a hot reload of this file.
16const agents = atom({ plugin: 'agent-radar', key: 'agents' } as const, [] as RadarAgent[])
17const selected = atom({ plugin: 'agent-radar', key: 'selected' } as const, null as string | null)
18const now = atom({ plugin: 'agent-radar', key: 'now' } as const, 0)
19const isEnabledRef = atom({ plugin: 'agent-radar', key: 'isEnabled' } as const, true)
20
21let bandOrder: Promise<string[]> | undefined // read once per session: a new session applies an edit
22
23export const register: Register = on => {
24 on('session.start', async ($, e, next) => {
25 bandOrder = undefined // a new session reads the order file again
26 // Published so the meus-mods panel reads the state, never a copied default (the toggle is per session).
27 await update($, isEnabledRef, () => isEnabledRef.initial)
28 const r = await next(e)
29 await $.command.register({ name: 'radar', description: 'Show what each subagent is doing, in the Agent Radar pane' }).catch(() => {}) // a name Claude Code already has is refused: start anyway
30 await $.command.register({ name: 'radar-toggle', description: 'Toggle Agent Radar on/off' }).catch(() => {})
31
32 // A clock for the elapsed times, ticking only while something runs.
33 $.clock.every(1000, () => {
34 void (async () => {
35 // A few seconds past the 30 s too, so the redraw that drops a finished line happens.
36 if ((await read($, agents)).some(a => isShown(a, Date.now() - 3_000))) await update($, now, () => Date.now())
37 })().catch(() => {})
38 })
39 return r
40 })
41
42 on('agent.spawn', async ($, e, next) => {
43 const r = await next(e)
44 if (r.agentId) {
45 const id = r.agentId
46 const one: RadarAgent = { id, description: e.description || e.name || e.subagentType, type: e.subagentType, model: r.model, tokens: 0, status: 'running', startedAt: Date.now(), tools: 0, last: 'starting' }
47 await update($, agents, list => [one, ...list.filter(a => a.id !== id)].slice(0, 30))
48 }
49 return r
50 })
51
52 on('tool.call', async ($, e, next) => {
53 if (!e.agentId) return next(e)
54 const id = e.agentId
55 const line = describe(e as unknown as Record<string, unknown>)
56 // Only a TodoWrite changes the progress here; any other call leaves it as it was.
57 const progress = e.tool === 'TodoWrite' ? todoProgress(e.todos) : undefined
58 const bump = (a: RadarAgent): RadarAgent => ({ ...a, tools: a.tools + 1, last: line, progress: progress ?? a.progress })
59 if (e.tool !== 'TaskCreate' && e.tool !== 'TaskUpdate') {
60 // fail open on the radar's write only: a catch around the handler would rerun the tool if next threw
61 await update($, agents, list => list.map(a => (a.id === id ? bump(a) : a))).catch(() => {})
62 return next(e)
63 }
64 // TaskCreate's id exists only in its result, so the Task tools count once the call has answered, in one update.
65 const r = await next(e)
66 const change = taskChange(e as unknown as Record<string, unknown>, r)
67 await update($, agents, list => list.map(a => (a.id === id ? (change ? withTask(bump(a), change) : bump(a)) : a))).catch(() => {}) // never lose the tool's result over the radar
68 return r
69 })
70
71 on('turn.step', async function* ($, e, next) {
72 const r = yield* next(e) // usage rides on the step's result, once the response is whole
73 if (e.agentId && r.usage) {
74 const id = e.agentId
75 const used = sumTokens(r.usage)
76 await update($, agents, list => list.map(a => (a.id === id ? { ...a, tokens: a.tokens + used } : a)))
77 }
78 return r
79 }).catch(async function* ($, e, next) {
80 return yield* next(e) // fail open: a radar error never breaks the step
81 })
82
83 on('turn.complete', async ($, e, next) => {
84 const r = await next(e)
85 if (e.agentId) {
86 const id = e.agentId
87 const failed = e.reason === 'error' || e.reason === 'aborted'
88 if (e.usage) {
89 // The turn's usage already sums its steps, so adding it to the step counts would double: take the larger.
90 const total = sumTokens(e.usage)
91 await update($, agents, list => list.map(a => (a.id === id ? { ...a, tokens: Math.max(a.tokens, total) } : a)))
92 }
93 const list = await update($, agents, all =>
94 all.map(a => (a.id === id ? { ...a, status: failed ? ('failed' as const) : ('done' as const), endedAt: Date.now(), last: failed ? `stopped: ${e.reason}` : 'finished' } : a)),
95 )
96 const one = list.find(a => a.id === id)
97 if (one) $.ui.toast(`${failed ? '✗' : '✓'} ${one.description} ${failed ? 'failed' : 'done'} (${elapsed(one)} · ${one.tools} tools)`)
98 }
99 return r
100 }).catch(($, e, next) => next(e)) // fail open: a radar error never breaks the turn
101
102 on('command.run', { command: 'radar' }, async $ => {
103 if (!(await read($, isEnabledRef).catch(() => true))) return { text: 'Agent Radar está desativado. Use /radar-toggle para ativar.' }
104 await adopt($)
105 await update($, selected, () => null)
106 await $.ui.open({ id: PANE, title: 'Agent Radar', focus: true })
107 const list = await read($, agents)
108 const running = list.filter(a => a.status === 'running').length
109 return { text: `${running} running, ${list.length - running} finished` }
110 })
111
112 on('command.run', { command: 'radar-toggle' }, async $ => {
113 const state = await update($, isEnabledRef, v => !v)
114 return { text: `agent-radar ${state ? 'ativado' : 'desativado'}.` }
115 })
116
117 // The band: one live line per running agent, and a ✓ for 30 s once one finishes.
118 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
119 if (!(await read($, isEnabledRef).catch(() => true))) return next(e)
120 const rest = await next(e) // what other mods and Claude Code draw here stays
121 await read($, now) // subscribes the band to the clock
122 const shown = (await read($, agents)).filter(a => isShown(a))
123 if (e.props.hasSurvey || shown.length === 0) return rest
124 const { Box, Text } = $.ui.resolve(e)
125 const mine = (
126 <Box flexDirection="column">
127 {shown.slice(0, BAND_ROWS).map(a => {
128 const m = meta(a)
129 return (
130 <Text key={a.id} wrap="truncate-end">
131 <Text color={color(a)} bold>{` ${icon(a)} ${a.description}`}</Text>
132 <Text dimColor>{` ${elapsed(a)} · ${a.tools} tools`}</Text>
133 {a.status === 'running' && <Text>{` · ${a.last}`}</Text>}
134 {a.status === 'failed' && <Text color="red">{` · ${a.last}`}</Text>}
135 {m && <Text dimColor>{` · ${m}`}</Text>}
136 {a.progress ? <Text>{` · ${a.progress}`}</Text> : null}
137 </Text>
138 )
139 })}
140 {shown.length > BAND_ROWS && <Text dimColor>{` +${shown.length - BAND_ROWS} more · /radar`}</Text>}
141 </Box>
142 )
143 return stackBands(rest, 'agent-radar', mine, 'above', await (bandOrder ??= readBandOrder($).catch(() => []))) // above what other mods draw, unless the band order says otherwise
144 })
145
146 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
147 const { Box, Text, Button } = $.ui.resolve(e)
148 await read($, now) // subscribes the pane to the clock
149 const list = await read($, agents)
150 const pick = await read($, selected)
151 const one = list.find(a => a.id === pick)
152
153 if (one) {
154 const found = await $.session.messages({ agentId: one.id })
155 const messages = 'deny' in found ? [] : found.slice(-6)
156 return (
157 <Box flexDirection="column">
158 <Text>
159 <Text color={color(one)} bold>{`${icon(one)} ${one.description}`}</Text>
160 <Text dimColor>{` ${one.type} · ${elapsed(one)} · ${one.tools} tools`}</Text>
161 </Text>
162 <Text dimColor wrap="truncate-end">{`now: ${one.last}`}</Text>
163 <Text dimColor>{'─'.repeat(Math.max(10, e.props.bodyColumns - 2))}</Text>
164 {messages.length === 0 && <Text dimColor>No messages yet.</Text>}
165 {messages.map(m => (
166 <Text wrap="truncate-end">
167 <Text color={m.role === 'assistant' ? 'cyan' : undefined} bold>{m.role === 'assistant' ? 'agent: ' : 'task: '}</Text>
168 <Text>{m.text.replace(/\s+/g, ' ').slice(0, 400) || '(tool calls)'}</Text>
169 </Text>
170 ))}
171 <Box flexDirection="row" gap={1}>
172 <Button key="back" label="Back" hotkey="b" variant="primary" onPress={() => update($, selected, () => null)} />
173 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
174 </Box>
175 </Box>
176 )
177 }
178
179 const running = list.filter(a => a.status === 'running').length
180 return (
181 <Box flexDirection="column">
182 <Text bold>{`${running} running · ${list.length - running} finished`}</Text>
183 {list.length === 0 && <Text dimColor>No subagents yet this session.</Text>}
184 {list.slice(0, 9).map((a, i) => (
185 <Button
186 key={`agent-${a.id}`}
187 plain
188 hotkey={String(i + 1)}
189 dimColor={a.status !== 'running'}
190 label={`${icon(a)} ${a.description} · ${elapsed(a)} · ${a.tools} tools${a.status === 'done' ? '' : ` · ${a.last}`}`}
191 onPress={() => update($, selected, () => a.id)}
192 />
193 ))}
194 {list.length > 9 && <Text dimColor>{`… ${list.length - 9} older`}</Text>}
195 <Box flexDirection="row" gap={1}>
196 <Button key="clear" label="Clear finished" hotkey="c" onPress={() => update($, agents, all => all.filter(x => x.status === 'running'))} />
197 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
198 </Box>
199 </Box>
200 )
201 })
202}
203
204// The band order file, as the claude-code-mods checklist saves it; [] on any error.
205async function readBandOrder($: EngineInterface): Promise<string[]> {
206 const file = bandsFile(await $.env.get('CLAUDE_CONFIG_DIR'), await $.env.get('HOME'))
207 return file && (await $.fs.exists(file)) ? parseOrder(await $.fs.read(file)) : []
208}
209
210// Agents that started before this mod loaded: take them from the engine's own list.
211async function adopt($: EngineInterface) {
212 const known = new Set((await read($, agents)).map(a => a.id))
213 const fresh = (await $.agent.list())
214 .filter(a => !known.has(a.id))
215 .map((a): RadarAgent => ({
216 id: a.id,
217 description: a.description,
218 type: a.type,
219 tokens: 0,
220 status: a.status === 'running' ? 'running' : a.status === 'completed' ? 'done' : 'failed',
221 startedAt: Date.now(),
222 tools: 0,
223 last: a.status === 'running' ? 'running (started before the radar)' : a.status,
224 }))
225 if (fresh.length > 0) await update($, agents, list => [...list, ...fresh])
226}
227
228// Running, or finished in the last 30 s.
229export function isShown(a: RadarAgent, at = Date.now()) {
230 return a.status === 'running' || (a.endedAt !== undefined && at - a.endedAt < SHOW_DONE_MS)
231}
232
233function icon(a: RadarAgent) {
234 return a.status === 'running' ? '●' : a.status === 'done' ? '✓' : '✗'
235}
236
237function color(a: RadarAgent) {
238 return a.status === 'running' ? 'yellow' : a.status === 'done' ? 'green' : 'red'
239}
240
241export function elapsed(a: Pick<RadarAgent, 'startedAt' | 'endedAt'>, at = Date.now()) {
242 const s = Math.max(0, Math.round(((a.endedAt ?? at) - a.startedAt) / 1000))
243 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
244}
245
246// Model and tokens, each only once the engine has reported it.
247function meta(a: RadarAgent) {
248 return [a.model, a.tokens > 0 ? formatTokens(a.tokens) : undefined].filter(Boolean).join(' · ')
249}
250
251// Tokens the step or turn processed fresh: uncached input, cache writes and output. Cache reads are left out:
252// every step re-reads the whole cached context, so summing them counts that context once per step.
253function sumTokens(u: { input_tokens?: number | null; output_tokens?: number | null; cache_creation_input_tokens?: number | null }) {
254 return (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0)
255}
256
257// "3/7 tarefas" from a TodoWrite list; undefined when the input is not a list.
258function todoProgress(todos: unknown) {
259 if (!Array.isArray(todos)) return undefined
260 return `${todos.filter(t => t?.status === 'completed').length}/${todos.length} tarefas`
261}
262
263// What a TaskCreate or TaskUpdate did to the agent's task list: [task id, status]; undefined for any other call.
264function taskChange(e: Record<string, unknown>, r: { deny?: string; isError?: boolean; result?: unknown }): [string, string] | undefined {
265 if (r.deny !== undefined || r.isError) return undefined
266 const created = (r.result as { task?: { id?: unknown } } | undefined)?.task?.id
267 if (e.tool === 'TaskCreate' && typeof created === 'string') return [created, 'pending']
268 if (e.tool === 'TaskUpdate' && typeof e.taskId === 'string' && typeof e.status === 'string') return [e.taskId, e.status]
269 return undefined
270}
271
272function withTask(a: RadarAgent, [taskId, status]: [string, string]): RadarAgent {
273 const { [taskId]: _, ...rest } = a.tasks ?? {}
274 const tasks = status === 'deleted' ? rest : { ...rest, [taskId]: status }
275 const all = Object.values(tasks)
276 return { ...a, tasks, progress: all.length === 0 ? undefined : `${all.filter(s => s === 'completed').length}/${all.length} tarefas` }
277}
278
279function formatTokens(n: number) {
280 if (n < 1000) return `${n} tok`
281 return `${(Math.round(n / 100) / 10).toString()}k tok` // 1500 -> 1.5k, 2000 -> 2k, 42000 -> 42k
282}
283
284// A short label for one tool call: what a person would say the agent is doing.
285export function describe(e: Record<string, unknown>): string {
286 const tool = String(e.tool)
287 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
288 const file = (p: string) => p.split('/').slice(-2).join('/')
289 if (tool === 'Bash') return `running ${s('description') || s('command').slice(0, 50)}`
290 if (tool === 'Edit' || tool === 'Write') return `editing ${file(s('file_path'))}`
291 if (tool === 'Read') return `reading ${file(s('file_path'))}`
292 if (tool === 'Grep' || tool === 'Glob') return `searching ${s('pattern').slice(0, 30)}`
293 if (tool.includes('playwright')) return `browser: ${tool.split('__').pop()?.replace(/^browser_/, '')}`
294 if (tool.startsWith('mcp__')) return `using ${tool.split('__').slice(1).join(' ')}`
295 return `using ${tool}`
296}
297hooks/bands.ts 73 lines1// Band order: every band mod ships this same file (canonical copy: shared/bands.ts in the repo).
2// The order file, <configDir>/claude-code-mods/bands.json, lists mod names top to bottom
3// (the last sits right above the prompt). Each band mod wraps its band in a Box keyed
4// `band:<name>` and returns one column keyed `bands`; the next band mod unpacks that
5// column, adds its own band and sorts again, so the last one to draw sorts them all.
6// Edit shared/bands.ts, then copy it over every mod's hooks/bands.ts (a repo test checks).
7// Reading the file needs $, which Claude Code follows only within one file, so each mod
8// reads it in its own register file with bandsFile and parseOrder, once per session.
9import type { RenderElement, RenderNode } from 'claude-code'
10
11const COLUMN = 'bands'
12const PREFIX = 'band:'
13
14// Where the checklist saves the order: CLAUDE_CONFIG_DIR, or ~/.claude. Undefined with neither.
15export function bandsFile(configDir: string | undefined, home: string | undefined): string | undefined {
16 const dir = configDir ? configDir.replace(/\/+$/, '') : home ? `${home}/.claude` : ''
17 return dir ? `${dir}/claude-code-mods/bands.json` : undefined
18}
19
20// The names in an order file; [] when it is no JSON or not { order: string[] }.
21export function parseOrder(text: string): string[] {
22 try {
23 const order = (JSON.parse(text) as { order?: unknown } | null)?.order
24 return Array.isArray(order) && order.every(n => typeof n === 'string') ? order : []
25 } catch {
26 return []
27 }
28}
29
30/**
31 * One column of bands: `mine` wrapped in a Box keyed `band:<name>`, placed
32 * `at` (above or below) what `next(e)` returned, then sorted by `order`.
33 * With an order, items that are no band come first, then bands not in the
34 * order (in their current order), then the listed ones in file order. With
35 * no order nothing moves, so the screen looks as it did before.
36 */
37export function stackBands(rest: RenderElement | null | undefined, name: string, mine: RenderNode, at: 'above' | 'below', order: string[]): RenderElement {
38 const own: RenderElement = { type: 'Box', props: { key: PREFIX + name, flexDirection: 'column' }, children: [mine] }
39 const others = unpack(rest)
40 const items = at === 'above' ? [own, ...others] : [...others, own]
41 return { type: 'Box', props: { key: COLUMN, flexDirection: 'column' }, children: order.length ? sorted(items, order) : items }
42}
43
44// Another band mod's column gives up its items; anything else is one item.
45function unpack(rest: RenderElement | null | undefined): RenderNode[] {
46 if (!rest) return []
47 if (!isColumn(rest)) return [rest]
48 const kids = rest.children ?? []
49 const owner = rest.part?.plugin
50 // The engine stamped the column with its owner: that stamp belongs on the owner's own band.
51 return kids.map(k => (owner && typeof k !== 'string' && bandName(k) === owner && !('part' in k && k.part) ? { ...k, part: rest.part } : k))
52}
53
54function isColumn(el: RenderElement): el is RenderElement & { type: 'Box' } {
55 return el.type === 'Box' && el.props?.key === COLUMN
56}
57
58function bandName(node: RenderNode): string | undefined {
59 if (typeof node === 'string' || node.type !== 'Box') return undefined
60 const key = node.props?.key
61 return typeof key === 'string' && key.startsWith(PREFIX) ? key.slice(PREFIX.length) : undefined
62}
63
64function sorted(items: RenderNode[], order: string[]): RenderNode[] {
65 const rank = (n: RenderNode) => {
66 const name = bandName(n)
67 if (name === undefined) return -2 // not a band: first
68 const i = order.indexOf(name)
69 return i < 0 ? -1 : i // a band not in the order: on top of the listed ones
70 }
71 return items.map((n, i) => ({ n, i, r: rank(n) })).sort((a, b) => a.r - b.r || a.i - b.i).map(x => x.n)
72}
73types/index.d.ts 21 lines1export type RadarAgent = {
2 id: string
3 description: string
4 type: string
5 model?: string // left out when the engine does not report it
6 tokens: number
7 progress?: string
8 tasks?: Record<string, string> // TaskCreate/TaskUpdate: task id -> status
9 status: 'running' | 'done' | 'failed'
10 startedAt: number
11 endedAt?: number
12 tools: number
13 last: string
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'agent-radar': { agents: RadarAgent[]; selected: string | null; now: number; isEnabled: boolean }
19 }
20}
21