Claude explains what it is doing in Moroccan Darija, live above the prompt

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 83 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Line } from '../types'
5import { done, failed, narrate, started } from './darija'
6
7import { ALERT, TERRACOTTA, fringe } from './zrabi'
8
9const lines = atom({ plugin: 'darija-narrator', key: 'lines' } as const, [] as Line[])
10const isHidden = atom({ plugin: 'darija-narrator', key: 'isHidden' } as const, false)
11
12const KEEP = 3
13
14function push($: EngineInterface, line: Line) {
15 return update($, lines, all => [...all, line].slice(-KEEP))
16}
17
18export const register: Register = on => {
19 let startedAt = 0
20 let tools = 0
21
22 on('prompt.submit', async ($, e, next) => {
23 startedAt = await $.clock.now()
24 tools = 0
25 await push($, { text: started(e.text), isError: false })
26
27 return next(e)
28 })
29
30 on('tool.call', async ($, e, next) => {
31 tools += 1
32 const text = narrate(e.tool, e as Record<string, unknown>)
33 $.ui.status(text)
34 await push($, { text, isError: false })
35
36 const ran = await next(e)
37
38 if (ran.deny === undefined && ran.isError === true) {
39 const bad = failed(e.tool)
40 $.ui.toast(bad)
41 await push($, { text: bad, isError: true })
42 }
43
44 return ran
45 })
46
47 on('turn.complete', async ($, e, next) => {
48 const seconds = Math.round(((await $.clock.now()) - startedAt) / 1000)
49 $.ui.status(undefined)
50 await push($, { text: done(seconds, tools), isError: false })
51
52 return next(e)
53 })
54
55 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
56 const all = await read($, lines)
57
58 if (e.props.hasSurvey || all.length === 0 || (await read($, isHidden))) {
59 return next(e)
60 }
61
62 const { Box, Button, Text } = $.ui.resolve(e)
63 const width = e.props.bodyColumns - 2
64
65 return (
66 <Box flexDirection="column">
67 <Text color={TERRACOTTA}>{fringe(width)}</Text>
68 <Box flexDirection="column" paddingX={2}>
69 {all.map((l, i) => (
70 <Text key={`l${i}`} color={l.isError ? ALERT : undefined} dimColor={!l.isError && i < all.length - 1}>
71 {l.text}
72 </Text>
73 ))}
74 </Box>
75 <Box>
76 <Text color={TERRACOTTA}>{fringe(width - 10, 2)} </Text>
77 <Button key="hide" label="Khbbi" onPress={() => update($, isHidden, () => true)} />
78 </Box>
79 </Box>
80 )
81 })
82}
83hooks/darija.ts 137 lines1// Darija b l-7rouf latiniya: 3 = ع, 7 = ح, 9 = ق, gh = غ, kh = خ, ch = ش
2
3const short = (value: unknown, max = 50): string => {
4 const s = String(value ?? '').replace(/\s+/g, ' ').trim()
5
6 return s.length > max ? `${s.slice(0, max - 1)}…` : s
7}
8
9const base = (path: unknown): string => short(String(path ?? '').split('/').pop(), 40)
10
11// The same call always gets the same phrase; different calls rotate through the variants.
12const pick = (variants: readonly string[], seed: string): string => {
13 let h = 0
14
15 for (const c of seed) {
16 h = (h * 31 + c.charCodeAt(0)) >>> 0
17 }
18
19 return variants[h % variants.length]!
20}
21
22export const narrate = (tool: string, input: Record<string, unknown>): string => {
23 const seed = tool + JSON.stringify(input)
24
25 switch (tool) {
26 case 'Read': {
27 const f = base(input.file_path)
28
29 return pick(
30 [
31 `🔍 Kan9ra l-fichier ${f}`,
32 `🔍 Khllini nchouf ach kayn f ${f}`,
33 `🔍 Kandir chi tlla 3la ${f}`,
34 `🔍 Kan-f7ess ${f}`,
35 ],
36 seed,
37 )
38 }
39 case 'Edit':
40 case 'MultiEdit': {
41 const f = base(input.file_path)
42
43 return pick(
44 [`✏️ Kanbddel chwiya f ${f}`, `✏️ Kanssle7 ${f}`, `✏️ Kanzid lmsa f ${f}`, `✏️ Kan-reggel ${f}`],
45 seed,
46 )
47 }
48 case 'Write': {
49 const f = base(input.file_path)
50
51 return pick([`📝 Kanktb ${f}`, `📝 Kansayb fichier jdid: ${f}`, `📝 Kancriyi fichier jdid: ${f}`], seed)
52 }
53 case 'Bash': {
54 const c = short(input.command)
55
56 return pick(
57 [
58 `⚙️ Kandawer had l-commande: ${c}`,
59 `⚙️ Kan-lanci ${c}`,
60 `⚙️ Yallah n-jerreb: ${c}`,
61 `⚙️ Kan-khddem ${c}`,
62 ],
63 seed,
64 )
65 }
66 case 'Grep': {
67 const p = short(input.pattern, 30)
68
69 return pick(
70 [
71 `🕵️ Kan9elleb 3la "${p}" f l-code`,
72 `🕵️ Fin mkhbbi "${p}"? Kan9elleb...`,
73 `🕵️ Fin ghaber "${p}"? Kan-fttech...`,
74 ],
75 seed,
76 )
77 }
78 case 'Glob': {
79 const p = short(input.pattern, 30)
80
81 return pick([`📂 Kan9elleb 3la l-fichiyat: ${p}`, `📂 Kan-fttech 3la l-fichiyat: ${p}`], seed)
82 }
83 case 'WebSearch': {
84 const q = short(input.query)
85
86 return pick(
87 [`🌐 Kan9elleb f l-internet 3la: ${q}`, `🌐 Nsewlo Google: ${q}`, `🌐 Nchoufo Google ach kay9oul 3la: ${q}`],
88 seed,
89 )
90 }
91 case 'WebFetch': {
92 const u = short(input.url)
93
94 return pick([`🌐 Kanjib had s-saf7a: ${u}`, `🌐 Kan-ft7 had s-saf7a: ${u}`], seed)
95 }
96 case 'Task':
97 case 'Agent':
98 return pick(
99 [
100 `🤝 Kan3eyyet 3la wa7ed l-agent y3awnni`,
101 `🤝 Jbt m3aya wa7ed s-sa7eb y3awnni`,
102 `🤝 Jbt l-renforts y3awnouni`,
103 ],
104 seed,
105 )
106 case 'TodoWrite':
107 return pick([`📋 Kanrettb l-lista d l-khdma`, `📋 Kan-reggel l-lista d l-khdma`], seed)
108 default:
109 return pick([`🛠️ Kankhddem ${tool}`, `🛠️ Yallah, nkhddem ${tool}`], seed)
110 }
111}
112
113export const failed = (tool: string): string =>
114 pick(
115 [
116 `❌ Ouups! ${tool} ma-khdemch, ghadi nchouf achnou w9e3`,
117 `❌ Tra chi mochkil f ${tool}, mat9l9ch`,
118 `❌ Wili! ${tool} ma-khdemch, ghadi n-reggel l-9diyya`,
119 ],
120 tool + Date.now(),
121 )
122
123export const done = (seconds: number, tools: number): string =>
124 seconds < 60
125 ? pick(
126 [
127 `✅ Safi, salina! F ${seconds} tanya, w dert ${tools} d l-khdmat`,
128 `✅ Safi, salina f ramchat 3eyn! F ${seconds} tanya, w dert ${tools} d l-khdmat`,
129 ],
130 `${seconds}:${tools}`,
131 )
132 : `✅ Hamdoulillah, kmmlna! ${Math.round(seconds / 60)} d9i9a w ${tools} d l-khdmat`
133
134// Picked by the prompt's text, so each prompt gets its own greeting.
135export const started = (prompt: string): string =>
136 pick(['Wakha, daba nchouf... 🤔', 'Wakha sidi, daba n-chouf... 🤔', 'Zid, ha ana m3ak... 🤔'], prompt)
137hooks/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 Line = { text: string; isError: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'darija-narrator': { lines: Line[]; isHidden: boolean }
6 }
7}
8