Displays timestamp on each response and notices on screen when the responding model switches (including jev-router)

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 116 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const isEnabledRef = atom({ plugin: 'session-telemetry', key: 'isEnabled' } as const, true)
5
6export function stamp(ms: number): string {
7 const d = new Date(ms)
8 try {
9 return d.toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour12: false })
10 } catch {
11 // ponytail: no Intl timezone data, UTC is shown and labelled so it is never mistaken for local
12 return `${d.toISOString().slice(11, 19)} UTC`
13 }
14}
15
16export function switchLine(prev: string | undefined, curr: string, ms: number, reason?: string): string | null {
17 if (!prev || prev === curr) return null
18 return `⇄ ${stamp(ms)} modelo trocado: ${prev} → ${curr}` + (reason ? ` · motivo: ${reason}` : '')
19}
20
21// Why the jev-router sent the turn where it did (GET /route/<session>). Only behind
22// a router: with no ANTHROPIC_BASE_URL the request goes to Anthropic, and a /model
23// switch has no reason to fetch. Any failure leaves the line without a reason.
24async function routeReason($: EngineInterface): Promise<string | undefined> {
25 const base = await $.env.get('ANTHROPIC_BASE_URL')
26 if (!base) return undefined
27 try {
28 const res = await $.http.fetch(`${base.replace(/\/+$/, '')}/route/${encodeURIComponent(await $.session.id())}`)
29 if (!res.ok) return undefined
30 const reason = (JSON.parse(res.text) as { reason?: unknown }).reason
31 return typeof reason === 'string' ? reason : undefined
32 } catch {
33 return undefined
34 }
35}
36
37// O modelo que de fato respondeu vem no usage de cada requisição: é ali que
38// aparece o desvio silencioso do jev-router (pedido Opus, respondido gpt-oss),
39// e também a troca manual por /model — um detector cobre os dois casos.
40let lastModel: string | undefined
41
42export const register: Register = on => {
43 on('session.start', async ($, e, next) => {
44 const saved = await $.store.get('isEnabled').catch(() => undefined)
45 await update($, isEnabledRef, () => (typeof saved === 'boolean' ? saved : isEnabledRef.initial))
46 await $.command.register({
47 name: 'telemetria',
48 description: 'Liga/desliga a hora nas respostas e o aviso de troca de modelo.',
49 argumentHint: '<on|off>',
50 })
51 return next(e)
52 })
53
54 on('command.run', { command: 'telemetria' }, async ($, e) => {
55 const arg = e.args.trim().toLowerCase()
56 if (arg !== 'on' && arg !== 'off') return { text: 'Uso: /telemetria on|off' }
57 await update($, isEnabledRef, () => arg === 'on')
58 await $.store.set('isEnabled', arg === 'on') // $.state is per session; the store survives a restart
59 return { text: `session-telemetry ${arg === 'on' ? 'ativado' : 'desativado'}.` }
60 })
61
62 // Só a tela muda: a mensagem guardada e o que o modelo lê ficam intactos.
63 on('classic.MessageDisplay', async ($, e, next) => {
64 const res = await next(e)
65 if (e.index !== 0 || !(await read($, isEnabledRef))) return res
66 return { ...res, displayContent: `[${stamp(await $.clock.now())}] ${res.displayContent ?? e.delta}` }
67 })
68
69 // Um hook só: a API lança erro num segundo on('turn.step') do mesmo mod.
70 on('turn.step', async function* ($, e, next) {
71 // Subagentes usam outros modelos de propósito (Explore = Haiku); contar
72 // eles marcaria troca falsa a cada spawn.
73 if (e.agentId) return yield* next(e)
74
75 const sentAt = await $.clock.now()
76 let firstAt: number | null = null
77 let stopAt: number | null = null
78 const s = next(e)
79 for await (const c of s) {
80 if (c.kind === 'stop') stopAt = await $.clock.now()
81 else if (firstAt === null && c.kind !== 'engine') firstAt = await $.clock.now()
82 yield c
83 }
84 const res = await s.result
85 if (!(await read($, isEnabledRef))) {
86 if (res.usage?.model) lastModel = res.usage.model
87 return res
88 }
89
90 const perf = speedLine(sentAt, firstAt, stopAt ?? (await $.clock.now()), res.usage?.output_tokens ?? null)
91 if (perf) $.ui.log(perf)
92
93 const model = res.usage?.model
94 if (!model) return res
95 const switched = lastModel !== undefined && lastModel !== model
96 const line = switchLine(lastModel, model, await $.clock.now(), switched ? await routeReason($) : undefined)
97 lastModel = model
98 if (line) {
99 $.ui.log(line)
100 $.ui.toast(line, { timeoutMs: 8000 })
101 }
102 return res
103 })
104}
105
106// TTFT e tok/s de um passo, com as guardas do stepSpeed do Flightdeck: sem
107// primeiro token não há linha; sem tokens ou com tempo zero a taxa some em
108// vez de virar Infinity.
109export function speedLine(sentAt: number, firstAt: number | null, stopAt: number, outTokens: number | null): string | null {
110 if (firstAt === null) return null
111 const ttft = Math.max(0, firstAt - sentAt)
112 const decodeMs = stopAt - firstAt
113 const tps = outTokens !== null && outTokens > 0 && decodeMs > 0 ? (outTokens * 1000) / decodeMs : null
114 return `⚡ TTFT ${ttft}ms` + (tps !== null ? ` · ${tps.toFixed(1)} tok/s` : '')
115}
116types/index.d.ts 9 lines1// A contract may export only types; this one keeps the file a module.
2export type SessionTelemetryState = { isEnabled: boolean }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'session-telemetry': { isEnabled: boolean }
7 }
8}
9