Hajib: a doorman that stops dangerous commands and asks for approval in Moroccan Darija

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.ts 30 lines1import type { Register } from 'claude-code'
2
3import { classify } from './hajib'
4
5export const register: Register = on => {
6 on('tool.check', async ($, e, next) => {
7 const found = classify(e.tool, e.input)
8
9 if (found === null) {
10 return next(e)
11 }
12
13 if (found.verdict === 'deny') {
14 $.ui.toast('L-Hajib: l-bab msdoud, ma-kaynch douz ๐')
15
16 return { decision: 'deny', reason: found.reason }
17 }
18
19 const base = await next(e)
20
21 if (base.decision === 'deny') {
22 return base
23 }
24
25 $.ui.toast('L-Hajib: tsenna chwiya, khassni rayek โ')
26
27 return { ...base, decision: 'ask', reason: found.reason }
28 })
29}
30hooks/hajib.ts 53 lines1export type Verdict = { verdict: 'ask' | 'deny'; reason: string }
2
3const DENY: [RegExp, string][] = [
4 [/\brm\s+(-\w+\s+)+(\/|\/\*|~|\$HOME)(\s|$)/, 'Wqef 3ndek! Had l-commande ghadi tmse7 kolchi f l-machine'],
5 [/\bmkfs(\.\w+)?\b/, 'Wqef 3ndek! Had l-commande ghadi tformati l-disque'],
6 [/\bdd\b[^|;]*\bof=\/dev\//, 'Wqef 3ndek! Had l-commande ghadi tktb fo9 l-disque kaml'],
7 [/:\(\)\s*\{\s*:\|:&\s*\};:/, 'Wqef 3ndek! Hadi fork bomb, ghadi t7bs l-machine'],
8]
9
10const ASK: [RegExp, string][] = [
11 [/\brm\s+(-\w+\s+)*-\w*[rR]/, 'Had l-commande ghadi tmse7 dossiers kamlin. N-khllih ydouz?'],
12 [/\bgit\s+push\b[^|;]*(--force\b|--force-with-lease\b|\s-f\b)/, 'Force push! Ghadi ybddel l-historique li f remote. N-khllih ydouz?'],
13 [/\bgit\s+reset\s+--hard\b/, 'git reset --hard ghadi ydiyye3 ga3 l-ta3dilat li ma-tcommitawch. N-khllih ydouz?'],
14 [/\bgit\s+clean\s+-\w*f/, 'git clean ghadi yms7 l-fichiyat li ma-m-trackyinch. N-khllih ydouz?'],
15 [/\bsudo\b/, 'Claude bgha ykhddem sudo b sla7iyat l-admin. N-khllih ydouz?'],
16 [/(curl|wget)\b[^|;]*\|\s*(sudo\s+)?(ba|z)?sh\b/, 'Script jay mn l-internet w ghadi ytlan9 direct. N-khllih ydouz?'],
17 [/\bdrop\s+(table|database)\b/i, 'DROP: ghadi ytms7ou tables wla base de donnรฉes kamla. N-khllih ydouz?'],
18 [/\bchmod\s+(-\w+\s+)*777\b/, 'chmod 777 kay3ti l-ga3 l-7e9 ybddlou l-fichiyat. N-khllih ydouz?'],
19 [/\b(npm|pnpm|yarn)\s+publish\b/, 'Ghadi ytnchr l-package l-l-3alam kaml. N-khllih ydouz?'],
20]
21
22const SECRET = /(^|\/)(\.env(\.[\w.]+)?|id_rsa|id_ed25519|credentials|\.npmrc)$|\/\.ssh\//
23
24export const classify = (tool: string, input: unknown): Verdict | null => {
25 const args = (input ?? {}) as Record<string, unknown>
26
27 if (tool === 'Bash') {
28 const command = String(args.command ?? '')
29
30 for (const [re, reason] of DENY) {
31 if (re.test(command)) {
32 return { verdict: 'deny', reason: `๐ซ ${reason}` }
33 }
34 }
35
36 for (const [re, reason] of ASK) {
37 if (re.test(command)) {
38 return { verdict: 'ask', reason: `๐ ${reason}` }
39 }
40 }
41
42 return null
43 }
44
45 if (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit') {
46 return SECRET.test(String(args.file_path ?? ''))
47 ? { verdict: 'ask', reason: '๐ Had l-fichier fih l-asrar (secrets). N-khllih ybddel fih?' }
48 : null
49 }
50
51 return null
52}
53