Brrad: when a task takes a while, tells you in Darija to take a break and make tea, then calls you back when it's done

Claude Code mods that speak Moroccan Darija, edged like a zrabi (Berber rug).
Claude kaykhddem, w nta katchrb atay. ๐ซ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ซ 4:32 l-brrad kaytbokh... ๐ซ
Atay wjed? Khwi lik kas tani, ana mazal khddam
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ [Khbbi]
claude plugin marketplace add Mohamedballouch/darija-mods
claude plugin install darija-narrator@darija-mods
claude plugin install zlafa@darija-mods
claude plugin install hajib@darija-mods
claude plugin install brrad@darija-mods
Install all four, or only the ones you like.
| Mod | What it does | Example | |
|---|---|---|---|
| darija-narrator | Narrates every step Claude takes, live above the prompt | ๐ Khllini nchouf ach kayn f app.ts | |
| zlafa (ุงูุฒูุงูุฉ) | A bowl that fills as the context window fills; warns at 80% and 95% | ๐ฅฃ [โโโโโโโโโโโโ] 82% L-zlafa 9rbat t3mer, fkker f /compact | |
| hajib (ุงูุญุงุฌุจ) | Blocks catastrophic commands (wiping /, formatting a diskโฆ) and asks before risky ones (git push --force, sudo, `curl \ | sh, editing .env`โฆ) | ๐ Force push! Ghadi ybddel l-historique li f remote. N-khllih ydouz? |
| brrad (ุงูุจุฑุงุฏ) | When a task runs long, tells you to step away for tea; calls you back when it's done and counts your cups for the day | Hadchi ghadi ytwl chwiya... khud lik pause w chi brrad d atay ๐ซ |
Hajib only ever makes approvals stricter: it never auto-approves anything.
In the plugin config menu:
90; later nudges (4 min, 8 min, 15 min) scale from itDarija is written in Latin letters with the usual Arabizi numbers: 3 = ุน ยท 7 = ุญ ยท 9 = ู ยท gh = ุบ ยท kh = ุฎ ยท ch = ุด
Every band sits between two rows of rug fringe (โโโโโ) in terracotta #B5452A. The text keeps your terminal's own colours, so it reads the same on light and dark themes; only warnings turn red.
Each mod is a folder with .claude-plugin/plugin.json and a hooks module under hooks/.
claude plugin validate ./brrad
claude plugin test ./brrad
claude --plugin-dir ./brrad
Better Darija wording is very welcome: the phrases live in darija-narrator/hooks/darija.ts, zlafa/hooks/zlafa.ts, hajib/hooks/hajib.ts and brrad/hooks/brrad.ts.
MIT
hooks/register.tsx 114 lines1import { atom, read, update } from 'claude-code'
2import type { Register, Timer } from 'claude-code'
3
4import { back, band, heads, isSlow, nudge, stepAt, tally } from './brrad'
5
6import { TERRACOTTA, fringe } from './zrabi'
7
8const elapsed = atom({ plugin: 'brrad', key: 'elapsed' } as const, null as number | null)
9const isHidden = atom({ plugin: 'brrad', key: 'isHidden' } as const, false)
10
11const TICK_MS = 5000
12
13export const register: Register = (on, options) => {
14 const first = Math.max(15, Number(options.firstNudgeSeconds ?? 90))
15 const isQuiet = options.quiet === true
16
17 let startedAt = 0
18 let step = -1
19 let agents = 0
20 let timer: Timer | undefined
21
22 on('prompt.submit', async ($, e, next) => {
23 timer?.cancel()
24 startedAt = await $.clock.now()
25 step = -1
26 agents = 0
27 await update($, elapsed, () => null)
28 await update($, isHidden, () => false)
29
30 timer = $.clock.every(TICK_MS, async () => {
31 const seconds = ((await $.clock.now()) - startedAt) / 1000
32 const due = stepAt(seconds, first)
33
34 if (due > step) {
35 step = due
36
37 if (!isQuiet) {
38 $.ui.toast(nudge(due), { timeoutMs: 8000 })
39 }
40 }
41
42 if (step >= 0 && !isQuiet) {
43 await update($, elapsed, () => seconds)
44 }
45 })
46
47 return next(e)
48 })
49
50 on('tool.call', async ($, e, next) => {
51 const isEarly = step < 0
52 const isBuild = e.tool === 'Bash' && isSlow(String(e.command ?? ''))
53
54 if (e.tool === 'Agent') {
55 agents += 1
56 }
57
58 if (isEarly && (isBuild || agents >= 2)) {
59 step = 0
60
61 if (!isQuiet) {
62 $.ui.toast(isBuild ? heads.build : heads.agents, { timeoutMs: 8000 })
63 }
64 }
65
66 return next(e)
67 })
68
69 on('turn.complete', async ($, e, next) => {
70 timer?.cancel()
71 timer = undefined
72
73 const seconds = ((await $.clock.now()) - startedAt) / 1000
74 const wasBreak = step >= 0 || seconds >= first
75 await update($, elapsed, () => null)
76
77 if (wasBreak && !e.isAborted) {
78 const day = new Date(await $.clock.now()).toISOString().slice(0, 10)
79 const cups = Number((await $.store.get(`cups:${day}`)) ?? 0) + 1
80 await $.store.set(`cups:${day}`, cups)
81
82 const isError = e.reason === 'error' || e.reason === 'refusal'
83 $.ui.toast(`${back(seconds, isError)} ยท ${tally(cups)}`, { timeoutMs: 10000 })
84 }
85
86 return next(e)
87 })
88
89 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90 const seconds = await read($, elapsed)
91
92 if (e.props.hasSurvey || seconds === null || (await read($, isHidden))) {
93 return next(e)
94 }
95
96 const { Box, Button, Text } = $.ui.resolve(e)
97 const width = e.props.bodyColumns - 2
98
99 return (
100 <Box flexDirection="column">
101 <Text color={TERRACOTTA}>{fringe(width)}</Text>
102 <Box flexDirection="column" paddingX={2}>
103 <Text bold>{band(seconds)}</Text>
104 {step >= 0 ? <Text dimColor>{nudge(step)}</Text> : null}
105 </Box>
106 <Box>
107 <Text color={TERRACOTTA}>{fringe(width - 10, 2)} </Text>
108 <Button key="hide" label="Khbbi" onPress={() => update($, isHidden, () => true)} />
109 </Box>
110 </Box>
111 )
112 })
113}
114hooks/brrad.ts 58 lines1// Darija b l-7rouf latiniya: 3 = ุน, 7 = ุญ, 9 = ู, gh = ุบ, kh = ุฎ, ch = ุด
2
3// Nudge points as multiples of the first one: 90s โ 90s, 4m, 8m, 15m.
4const STEPS = [1, 2.67, 5.33, 10] as const
5
6const NUDGES = [
7 'Hadchi ghadi ytwl chwiya... khud lik pause w chi brrad d atay ๐ซ',
8 'Atay wjed? Khwi lik kas tani, ana mazal khddam',
9 'Rah hadi khdma kbira, sir tmchcha chwiya w rje3',
10 'Ila kan 3ndek chi msemmen wla 7rcha, daba howa l-weqt ๐ฅ',
11] as const
12
13// The nudge index due at `seconds`, given the first threshold; -1 before the first.
14export const stepAt = (seconds: number, first: number): number => {
15 let step = -1
16
17 STEPS.forEach((k, i) => {
18 if (seconds >= first * k) {
19 step = i
20 }
21 })
22
23 return step
24}
25
26export const nudge = (step: number): string => NUDGES[Math.min(step, NUDGES.length - 1)]!
27
28const SLOW = /\b(npm|pnpm|yarn|bun)\s+(install|i|ci|run\s+build)\b|\bdocker\s+(build|compose\s+up)\b|\bcargo\s+(build|test)\b|\bgradlew?\s+build\b|\bmvn\s+(install|package)\b|\bpytest\b|\bgo\s+test\s+\.\/\.\.\.|\bpip\s+install\s+-r\b|\bmake\b(\s+-j\d*)?\s*$/
29
30export const isSlow = (command: string): boolean => SLOW.test(command.trim())
31
32export const heads = {
33 build: 'Bda l-khdma t9ila... hada weqt atay, ma-tb9ach tsenna ๐ซ',
34 agents: 'Jbt l-jma3a ykhdmou m3aya, ghadi ytwlo chwiya. Sir sawb atay ๐ซ',
35} as const
36
37export const teapot = (seconds: number): string => (seconds < 240 ? '๐ซ' : seconds < 600 ? 'โจ๏ธ' : 'โ')
38
39export const clock = (seconds: number): string => {
40 const s = Math.max(0, Math.floor(seconds))
41
42 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
43}
44
45export const band = (seconds: number): string => `${teapot(seconds)} ${clock(seconds)} l-brrad kaytbokh... ๐ซ`
46
47export const back = (seconds: number, isError: boolean): string =>
48 isError
49 ? 'Rje3, kayn chi mochkil khassni rayek fih โ ๏ธ'
50 : seconds >= 600
51 ? 'Salina! Atay brd? Machi mochkil, l-khdma t9adat โ
'
52 : 'Rje3 a sa7bi! Salina, w l-brrad mazal skhoun โ'
53
54export const tally = (cups: number): string =>
55 cups >= 5
56 ? `Lyoum chrbti ${cups} d l-kissan d atay ๐
bzzaf 3la l-9lb`
57 : `Lyoum chrbti ${cups} ${cups === 1 ? 'kas' : 'd l-kissan'} d atay`
58hooks/zrabi.ts 18 lines1// Zrabi (ุงูุฒุฑุจูุฉ): the diamond fringe of a Berber rug, in terracotta.
2
3export const TERRACOTTA = '#B5452A'
4export const ALERT = '#E5534B'
5
6const UNIT = 'โโโโ'
7
8// A fringe `width` cells wide that starts and ends on a diamond; `offset` 2 starts on โ.
9export const fringe = (width: number, offset = 0): string => {
10 let s = ''
11
12 for (let i = 0; i < Math.max(0, width); i++) {
13 s += UNIT[(i + offset) % UNIT.length]
14 }
15
16 return s.endsWith('โ') ? s.slice(0, -1) : s
17}
18types/index.d.ts 8 lines1export type Elapsed = number | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 brrad: { elapsed: Elapsed; isHidden: boolean }
6 }
7}
8