SLOPSHOPPER

zlafa

Zlafa: a bowl that fills as the context window fills, with warnings in Moroccan Darija

newbandguardtoast
v0.1.0MITupdated 2026-10-08Mohamedballouch/darija-mods/zlafa
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · zlafa
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇ 🥣 [██████░░░░░░] 49% Zlafa mazal khawya, khddem 3la khatrek ◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇ [ Khbbi ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇ 🥣 [██████░░░░░░] 49% Zlafa mazal khawya, khddem 3la khatrek ◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇─◆─◇ [ Khbbi ]
README

darija-mods 🇲🇦

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]

Install

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.

The mods

ModWhat it doesExample
darija-narratorNarrates 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 dayHadchi ghadi ytwl chwiya... khud lik pause w chi brrad d atay 🫖

Hajib only ever makes approvals stricter: it never auto-approves anything.

Brrad settings

In the plugin config menu:

  • First tea nudge (seconds): default 90; later nudges (4 min, 8 min, 15 min) scale from it
  • Quiet mode: only the "come back, it's done" toast

Spelling

Darija is written in Latin letters with the usual Arabizi numbers: 3 = ع · 7 = ح · 9 = ق · gh = غ · kh = خ · ch = ش

Look

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.

Develop

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.

License

MIT

Source 4 files
hooks/register.tsx 69 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { level, line, message } from './zlafa'
5
6import { ALERT, TERRACOTTA, fringe } from './zrabi'
7
8const percent = atom({ plugin: 'zlafa', key: 'percent' } as const, null as number | null)
9const lastLevel = atom({ plugin: 'zlafa', key: 'level' } as const, 0)
10const isHidden = atom({ plugin: 'zlafa', key: 'isHidden' } as const, false)
11
12async function refresh($: EngineInterface) {
13  const { context } = await $.session.usage()
14
15  if (context.percent === undefined) {
16    return
17  }
18
19  const p = context.percent
20  const before = await read($, lastLevel)
21  const now = level(p)
22
23  await update($, percent, () => p)
24  await update($, lastLevel, () => now)
25
26  if (now > before && now >= 2) {
27    $.ui.toast(`🥣 ${message(p)}`)
28  }
29}
30
31export const register: Register = on => {
32  on('tool.call', async ($, e, next) => {
33    const ran = await next(e)
34    await refresh($).catch(() => undefined)
35
36    return ran
37  })
38
39  on('turn.complete', async ($, e, next) => {
40    await refresh($).catch(() => undefined)
41
42    return next(e)
43  })
44
45  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
46    const p = await read($, percent)
47
48    if (e.props.hasSurvey || p === null || (await read($, isHidden))) {
49      return next(e)
50    }
51
52    const { Box, Button, Text } = $.ui.resolve(e)
53    const width = e.props.bodyColumns - 2
54
55    return (
56      <Box flexDirection="column">
57        <Text color={TERRACOTTA}>{fringe(width)}</Text>
58        <Box paddingX={2}>
59          <Text color={level(p) >= 2 ? ALERT : undefined}>{line(p)}</Text>
60        </Box>
61        <Box>
62          <Text color={TERRACOTTA}>{fringe(width - 10, 2)} </Text>
63          <Button key="hide" label="Khbbi" onPress={() => update($, isHidden, () => true)} />
64        </Box>
65      </Box>
66    )
67  })
68}
69
hooks/zlafa.ts 22 lines
1export const bar = (percent: number, width = 12): string => {
2  const p = Math.max(0, Math.min(100, Math.round(percent)))
3  const filled = Math.round((p / 100) * width)
4
5  return '█'.repeat(filled) + '░'.repeat(width - filled)
6}
7
8// 0 = fargha, 1 = kat3mar, 2 = kadi3, 3 = tfid
9export const level = (percent: number): 0 | 1 | 2 | 3 =>
10  percent >= 95 ? 3 : percent >= 80 ? 2 : percent >= 50 ? 1 : 0
11
12export const message = (percent: number): string =>
13  [
14    'Zlafa mazal khawya, khddem 3la khatrek',
15    'Zlafa bdat kat3mer, walakin kolchi mazal mzyan',
16    'Zlafa 9rbat t3mer, red l-bal w fkker f /compact',
17    'Safi, ghadi tfid! Dir /compact daba serbi',
18  ][level(percent)]!
19
20export const line = (percent: number): string =>
21  `🥣 [${bar(percent)}] ${Math.round(percent)}%  ${message(percent)}`
22
hooks/zrabi.ts 18 lines
1// 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}
18
types/index.d.ts 8 lines
1export type Percent = number | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    zlafa: { percent: Percent; level: number; isHidden: boolean }
6  }
7}
8