Shows that the mogger guard hooks are running: status line, a bar above the prompt, and a toast when mogger blocks or warns.

Shows that mogger is running and helping. It adds three things to the Claude Code screen:
mogger: N guards fired, M blocked (counted since the session started).Last: blocked a secret in config.js (12s ago). It turns dim after 60 seconds of quiet. It has a Hide button. It is not drawn when there is no log yet.Each mogger guard hook calls mogger_event (in hooks/scripts/lib.sh). That adds one line to .claude/state/mogger-events.log in your project:
<epoch seconds> TAB <kind> TAB <short message>
kind is block, warn, ok (an action ran, such as auto-format or a checkpoint) or info. Messages use file basenames only: no command text, no secrets, no full paths. The log is cut to about the last 200 lines. If the log cannot be written, the hook goes on as before: logging never changes a hook's exit code or output. The log is kept out of checkpoints.
The mod reads that file with $.fs every 2 seconds. It does not read hook results directly.
Mods need a Claude Code version that supports them (see the mods blog post). This mod is opt-in: installing mogger does not turn it on.
Try it for one session:
claude --plugin-dir /path/to/claude-mogger/mods/mogger-status
Check it first:
claude plugin validate mods/mogger-status
claude plugin test mods/mogger-status
Mods are not sandboxed. A mod runs code inside your Claude Code session with the same access as Claude Code itself. Read hooks/register.tsx and hooks/log.ts before you enable it. This one only reads .claude/state/mogger-events.log, and shows text. It does not write files, run commands or use the network.
hooks/register.tsx 82 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { MoggerSummary } from '../types'
5import { LOG_PATH, ageText, newBlocks, newestTs, parseLog, statusText, summarize } from './log'
6
7const summaryAtom = atom({ plugin: 'mogger-status', key: 'summary' } as const, null as MoggerSummary | null)
8const isHidden = atom({ plugin: 'mogger-status', key: 'isHidden' } as const, false)
9const sinceAtom = atom({ plugin: 'mogger-status', key: 'since' } as const, 0)
10
11const QUIET_MS = 60_000
12const POLL_MS = 2000
13
14// newest event time already seen (seconds); -1 until the first read, so
15// old blocks from earlier sessions never toast.
16let seenTs = -1
17
18async function refresh($: EngineInterface): Promise<void> {
19 let text = ''
20 try {
21 text = await $.fs.read(LOG_PATH)
22 } catch {
23 // no log yet: show nothing
24 $.ui.status(undefined)
25 await update($, summaryAtom, () => null)
26 return
27 }
28 const events = parseLog(text)
29 let since = await read($, sinceAtom)
30 if (since === 0) {
31 since = await $.clock.now()
32 await update($, sinceAtom, () => since)
33 }
34 const s = summarize(events, since)
35 if (seenTs >= 0) {
36 for (const b of newBlocks(events, seenTs)) {
37 $.ui.toast(`mogger ${b.msg}`)
38 }
39 }
40 seenTs = newestTs(events)
41 await update($, summaryAtom, () => s)
42 $.ui.status(statusText(s))
43}
44
45export const register: Register = on => {
46 on('session.start', async ($, e, next) => {
47 await refresh($)
48 $.clock.every(POLL_MS, () => refresh($))
49 return next(e)
50 })
51
52 on('turn.complete', async ($, e, next) => {
53 await refresh($)
54 return next(e)
55 })
56
57 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
58 const s = await read($, summaryAtom)
59 if (e.props.hasSurvey || s === null || (await read($, isHidden))) {
60 return next(e)
61 }
62 const { Box, Button, Text } = $.ui.resolve(e)
63 const now = await $.clock.now()
64 const last = s.last
65 const isQuiet = last === null || now - last.ts * 1000 > QUIET_MS
66 const color = last === null ? undefined : last.kind === 'block' ? 'red' : last.kind === 'warn' ? 'yellow' : 'green'
67 const line =
68 last === null
69 ? `${statusText(s)}. Nothing to report yet.`
70 : `${statusText(s)}. Last: ${last.msg} (${ageText(now, last.ts)})`
71
72 return (
73 <Box>
74 <Text dimColor={isQuiet} color={isQuiet ? undefined : color}>
75 {line}{' '}
76 </Text>
77 <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
78 </Box>
79 )
80 })
81}
82hooks/log.ts 55 lines1// Pure helpers for the mogger event log. One line per event:
2// <epoch seconds> TAB <kind> TAB <short message>
3// Written by mogger_event in hooks/scripts/lib.sh. No secrets in the log.
4import type { MoggerEvent, MoggerSummary } from '../types'
5
6export const LOG_PATH = '.claude/state/mogger-events.log'
7
8// kinds that count as "a guard fired". "info" lines (session start) do not.
9const COUNTED = ['block', 'warn', 'ok']
10
11export function parseLog(text: string): MoggerEvent[] {
12 const events: MoggerEvent[] = []
13 for (const line of text.split('\n')) {
14 const parts = line.split('\t')
15 if (parts.length < 3) continue
16 const ts = Number(parts[0])
17 if (!Number.isFinite(ts) || ts <= 0) continue
18 const kind = parts[1] ?? ''
19 if (kind === '') continue
20 events.push({ ts, kind, msg: parts.slice(2).join(' ').slice(0, 120) })
21 }
22 return events
23}
24
25// sinceMs: only count events at or after this time (0 = all).
26export function summarize(events: MoggerEvent[], sinceMs: number): MoggerSummary {
27 const mine = events.filter(e => e.ts * 1000 >= sinceMs)
28 const counted = mine.filter(e => COUNTED.includes(e.kind))
29 return {
30 fired: counted.length,
31 blocked: counted.filter(e => e.kind === 'block').length,
32 last: counted.length > 0 ? (counted[counted.length - 1] as MoggerEvent) : null,
33 }
34}
35
36export function statusText(s: MoggerSummary): string {
37 return `mogger: ${s.fired} guards fired, ${s.blocked} blocked`
38}
39
40export function newestTs(events: MoggerEvent[]): number {
41 return events.reduce((m, e) => Math.max(m, e.ts), 0)
42}
43
44// Events to toast: blocks newer than lastTs.
45export function newBlocks(events: MoggerEvent[], lastTs: number): MoggerEvent[] {
46 return events.filter(e => e.kind === 'block' && e.ts > lastTs)
47}
48
49export function ageText(nowMs: number, ts: number): string {
50 const s = Math.max(0, Math.round(nowMs / 1000 - ts))
51 if (s < 60) return `${s}s ago`
52 if (s < 3600) return `${Math.floor(s / 60)}m ago`
53 return `${Math.floor(s / 3600)}h ago`
54}
55types/index.d.ts 17 lines1export type MoggerEvent = { ts: number; kind: string; msg: string }
2export type MoggerSummary = {
3 fired: number
4 blocked: number
5 last: MoggerEvent | null
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'mogger-status': {
11 summary: MoggerSummary | null
12 isHidden: boolean
13 since: number
14 }
15 }
16}
17