Rewrites Bash calls instead of letting them fail: quotes unquoted --include=* globs, adds pipefail to verify pipes, and moves a foreground full verify into the…

Coding agents write code fast. mexxi makes them prove it works.
A coding agent is an AI tool that works in your repository: it reads the code, runs commands and makes changes. Give the one you already use, Claude Code, Codex or OpenCode, a task in plain words. You get back one pull request that passes your repository's own checks, with screenshots from a real browser and a plain call on whether it is safe to merge. You still decide: mexxi never merges. It runs on your machine, with the agent and subscription you already have, and needs no service or account of its own.
mexxi is Maltese for "to lead, to manage". The names explains the rest of the Maltese.
Coding agents can now take a whole task: read the code, make the change and open a pull request. That moves the slow part of the job. Writing the code is quick; trusting it is not.
What an agent hands back on its own is hard to trust:
So you either read every line, which costs the time the agent saved, or merge on trust.
mexxi moves the checking into the run, so the pull request arrives with its proof. The ideas behind mexxi explains each one.
npm run verify by default, must pass before work starts and again at the end. ibni, the command-line tool the agent runs, runs those checks itself and records the exact code they passed on, and it refuses to push any other code. A later change has to be checked and recorded again.To see the difference, compare two pull requests for the same request in a small demo app: without mexxi and with mexxi. Reviewing a mexxi pull request explains each part and what to check in a minute.
You need one of Claude Code, Codex or OpenCode, Node.js 22.13 or newer, and Git. You also need the forge's CLI, signed in, to open the pull request: gh for GitHub or glab for GitLab.
Three tools are optional. The Playwright MCP server lets the agent drive a browser itself; without it, the pull request's evidence says which checks did not run. The review tool OpenCodeReview (ocr) picks what the reviewer looks at. ffmpeg makes GIFs.
Paste this into your agent to install mexxi. It checks the prerequisites, asks what it cannot detect, and shows what it will change before it changes anything:
Set up mexxi for me. Read https://raw.githubusercontent.com/alexportelli12/mexxi/main/skills/ibda/SKILL.md and follow it.
Then paste this in a repository to prepare it. /lesti opens one pull request that adds what mexxi needs, such as the verify command, and fixes the checks that already fail.
Make this repository ready for mexxi: run /lesti here.
From the repository's default branch, type this in your agent's chat to ask for a change with /wettaq, the command that builds it:
/wettaq show the last sync time on the settings page
The setup guide covers installing by hand and what each prerequisite is for.
Coding agents load their instructions as commands you type, agents they can hand part of a task to, and skills they load only when a task needs them. The glossary explains each term.
/wettaq builds a change, /ittestja tests a UI by hand in a browser, and /lesti gets a repository ready. Three more look after the workflow itself: /fassal writes UX principles, /sewwi changes the workflow and /irrifletti looks back at sessions.ibni, which takes each task's worktree from prepare to publish, with step timings./irrifletti measures how recent sessions went, privately, on your machine.The same files serve Claude Code, Codex and OpenCode; Host details says what each one adds.
The tools are named in Maltese, the author's native language, like Verifika. Each name is an everyday word for the tool's job: /wettaq means "to carry out", /lesti "to get something ready" and ibni "to build". The names is a small dictionary of all of them.
The details live in the docs, generated from this repository so they cannot drift from it:
ibni CLI reference and every configuration key.Go for it. I made this repository public to share the workflow I use day to day. It is opinionated around how I like to work, so take the parts that fit and change the rest.
mexxi is released under the MIT licence. To send a change back, read CONTRIBUTING.md first.
The frontend-design and skill-creator skills are adapted from Anthropic's skills repository, under the Apache License 2.0. Each folder keeps its licence in LICENSE.txt.
hooks/register.ts 19 lines1// The command fixer: a Bash call that would trip over the shell is rewritten
2// on its way to the tool instead of failing, and the agent is told what
3// changed in the call's context. Claude Code only; the rules every host
4// follows live in the shared guard and instructions.
5import type { Register } from 'claude-code'
6
7import { fixCommand } from './fix'
8
9export const register: Register = on => {
10 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
11 const fix = fixCommand(e.command, e.run_in_background === true)
12 if (!fix.changes.length) return next(e)
13 const ran = await next({ ...e, command: fix.command, run_in_background: fix.runInBackground })
14 if (ran.deny !== undefined) return ran
15 const note = `${$.plugin.name} rewrote this command: ${fix.changes.join('; ')}. It ran as: ${fix.command}`
16 return { ...ran, context: [...(ran.context ?? []), note] }
17 })
18}
19hooks/fix.ts 156 lines1// Pure rewrites of a Bash command line, kept apart from the hook so tests can
2// call them directly. Each one fixes a failure the retro counted, and only
3// where the shell's own reading of the line is certain: anything inside
4// quotes is left alone.
5
6export type Fix = {
7 command: string
8 runInBackground: boolean
9 changes: string[]
10}
11
12type Scan = { index: number; quoted: boolean; wordStart: boolean }
13
14// `<<` or `<<-`, then the delimiter word, quoted or not. A here-string (`<<<`)
15// is excluded by the caller: its word is an ordinary one.
16const HEREDOC = /^<<(-?)[ \t]*((?:'[^'\n]*'|"[^"\n]*"|\\.|[^\s;&|<>()'"\\])+)/
17
18type Heredoc = { delimiter: string; stripTabs: boolean }
19
20// Marks the bodies of the heredocs opened on the line just ended as quoted, so
21// nothing in them is rewritten and their quotes never reach the scanner.
22// Returns where the scan resumes: the newline ending the last delimiter line.
23function skipHeredocBodies(command: string, start: number, heredocs: Heredoc[], positions: Scan[]): number {
24 const pending = [...heredocs]
25 let index = start
26 while (index < command.length) {
27 const newline = command.indexOf('\n', index)
28 const end = newline === -1 ? command.length : newline
29 const line = command.slice(index, end)
30 const { delimiter, stripTabs } = pending[0] ?? { delimiter: '', stripTabs: false }
31 if ((stripTabs ? line.replace(/^\t+/, '') : line) === delimiter) pending.shift()
32 for (let at = index; at < end; at += 1) positions.push({ index: at, quoted: true, wordStart: false })
33 if (pending.length === 0) return end
34 if (end < command.length) positions.push({ index: end, quoted: true, wordStart: false })
35 index = end + 1
36 }
37 return command.length
38}
39
40// Positions of the line with whether each sits inside quotes, and whether it
41// starts a word. Backslash escapes and heredoc bodies count as quoted.
42function scan(command: string): Scan[] {
43 const positions: Scan[] = []
44 let quote: '"' | "'" | null = null
45 let previous = ''
46 let heredocs: Heredoc[] = []
47 for (let index = 0; index < command.length; index += 1) {
48 const char = command[index] ?? ''
49 if (quote === null && char === '\n' && heredocs.length > 0) {
50 positions.push({ index, quoted: false, wordStart: false })
51 index = skipHeredocBodies(command, index + 1, heredocs, positions) - 1
52 heredocs = []
53 previous = '\n'
54 continue
55 }
56 const wordStart = quote === null && (index === 0 || /[\s;&|()]/.test(previous))
57 if (quote === null && command.startsWith('<<<', index)) {
58 for (let at = index; at < index + 3; at += 1) positions.push({ index: at, quoted: false, wordStart: at === index && wordStart })
59 previous = '<'
60 index += 2
61 continue
62 }
63 const heredoc = quote === null && char === '<' ? HEREDOC.exec(command.slice(index)) : null
64 if (heredoc) {
65 const word = heredoc[2] ?? ''
66 heredocs.push({ delimiter: word.replace(/'([^']*)'|"([^"]*)"|\\(.)/g, '$1$2$3'), stripTabs: heredoc[1] === '-' })
67 for (let at = index; at < index + heredoc[0].length; at += 1) positions.push({ index: at, quoted: true, wordStart: false })
68 previous = word[word.length - 1] ?? ''
69 index += heredoc[0].length - 1
70 continue
71 }
72 if (quote === null && char === '\\') {
73 positions.push({ index, quoted: true, wordStart }, { index: index + 1, quoted: true, wordStart: false })
74 previous = command[index + 1] ?? ''
75 index += 1
76 continue
77 }
78 if (quote === null && (char === '"' || char === "'")) quote = char
79 else if (quote !== null && char === quote) quote = null
80 positions.push({ index, quoted: quote !== null || char === '"' || char === "'", wordStart })
81 previous = char
82 }
83 return positions
84}
85
86const GLOB_OPTION = /^--(include|exclude)(-dir)?=([^\s'"\\$`;&|<>()]+)(?=[\s;&|<>()]|$)/
87
88const BRACE_LIST = /^([^{}]*)\{([^{}]*,[^{}]*)\}([^{}]*)$/
89
90// The globs a value stands for once the shell has expanded its braces, or null
91// when the braces are anything but one simple comma list (nested, a {1..3}
92// range): those are left for the shell rather than guessed at.
93function braceExpand(value: string): string[] | null {
94 if (!/[{}]/.test(value)) return [value]
95 const match = BRACE_LIST.exec(value)
96 if (!match) return null
97 return (match[2] ?? '').split(',').map((entry) => `${match[1]}${entry}${match[3]}`)
98}
99
100// zsh expands an unquoted --include=*.ts itself, and aborts the whole line with
101// "no matches found" when nothing in the directory matches. grep has no brace
102// syntax, so quoting --include=*.{ts,tsx} as it stands would match nothing;
103// it becomes one quoted option per entry, as bash's own expansion would.
104export function quoteGlobOptions(command: string): string {
105 const positions = scan(command)
106 let result = ''
107 let cursor = 0
108 for (const { index, quoted, wordStart } of positions) {
109 if (index < cursor || quoted || !wordStart) continue
110 const match = GLOB_OPTION.exec(command.slice(index))
111 if (!match || !/[*?[]/.test(match[3] ?? '')) continue
112 const globs = braceExpand(match[3] ?? '')
113 if (!globs) continue
114 const option = `--${match[1]}${match[2] ?? ''}`
115 result += `${command.slice(cursor, index)}${globs.map((glob) => `${option}='${glob}'`).join(' ')}`
116 cursor = index + match[0].length
117 }
118 return result + command.slice(cursor)
119}
120
121function hasUnquotedPipe(command: string): boolean {
122 return scan(command).some(({ index, quoted }) =>
123 !quoted && command[index] === '|' && command[index + 1] !== '|' && command[index - 1] !== '|')
124}
125
126const VERIFY = /(^|[\s;&|(])(npm run(?: -s| --silent)? verify|npx verifika|verifika|vfk)(?=\s|$|[;&|)])/
127const FULL_VERIFY = /(^|[\s;&|(])(npm run(?: -s| --silent)? verify|(?:npx )?verifika --all|vfk --all)(?=\s|$|[;&|)])/
128
129// A pipe reports its last command's status, so `npm run verify | tail` reads
130// green whatever verify did.
131export function addPipefail(command: string): string {
132 if (!VERIFY.test(command) || !hasUnquotedPipe(command) || /pipefail/.test(command)) return command
133 return `set -o pipefail; ${command}`
134}
135
136// A full verify outlasts the foreground Bash cap in slow repositories; in the
137// background the agent is told when it ends instead of polling or timing out.
138export function runsFullVerify(command: string): boolean {
139 const match = FULL_VERIFY.exec(command)
140 if (!match) return false
141 const at = match.index + (match[1]?.length ?? 0)
142 return !scan(command).some(({ index, quoted }) => index === at && quoted)
143}
144
145export function fixCommand(command: string, runInBackground: boolean): Fix {
146 const changes: string[] = []
147 let fixed = quoteGlobOptions(command)
148 if (fixed !== command) changes.push('quoted the --include/--exclude globs so the shell does not expand them')
149 const piped = addPipefail(fixed)
150 if (piped !== fixed) changes.push('added set -o pipefail so the pipe keeps verify\'s exit status')
151 fixed = piped
152 const background = !runInBackground && runsFullVerify(fixed)
153 if (background) changes.push('moved the full verify into the background: wait for its completion notification, never poll it or sleep')
154 return { command: fixed, runInBackground: runInBackground || background, changes }
155}
156