SLOPSHOPPER

command-fixer

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…

newguard
★ 2v0.1.0MITupdated 2026-10-21alexportelli12/mexxi/claude/mods/command-fixer
A shopper browsing a rack in a slop shop
README

mexxi

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.

The problem

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:

  • It skips tests. One run is careful; the next ships without proof, and says "all tests pass" either way.
  • It never looks at the UI. The code compiles, and nobody has opened the page.
  • It loses time to its environment. It retries the same failing command and hides failed steps.
  • Its pull request needs reverse-engineering. You read the whole diff to learn what it did and why.

So you either read every line, which costs the time the agent saved, or merge on trust.

How mexxi solves it

mexxi moves the checking into the run, so the pull request arrives with its proof. The ideas behind mexxi explains each one.

  • Each task gets its own copy of the repository, so your checkout stays untouched and tasks can run side by side.
  • A pass/fail command defines done. Your repository's checks, 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.
  • Somebody looks at the page. For UI changes the agent drives the running app in a real browser at phone, tablet and desktop sizes, and the pull request shows before and after.
  • A second reviewer judges it. A reviewer agent that cannot edit code judges substantial changes. It is an extra opinion; the checks and the browser are the proof.
  • Guardrails in code as well as in the rules. A hook backs up the rules against force pushes, merges and pushes to the default branch, and another stops an agent that keeps retrying a failing command.
  • The first screen does the explaining. The pull request opens with the merge call, the decisions only you can make, and the evidence: which checks ran, and which did not.

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.

Getting started

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.

What's in the box

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.

  • Commands you type in your agent: /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.
  • Agents: an independent code reviewer and a UI designer, each with the tools it needs and no more.
  • Skills: browser checks, repository mapping, commit and pull request writing, and the review step.
  • ibni, which takes each task's worktree from prepare to publish, with step timings.
  • Guardrails: rules forbid force pushes and merges, and a guard hook backs them up. When one command fails a third time, the agent must stop and rethink.
  • Retrospectives: /irrifletti measures how recent sessions went, privately, on your machine.
  • One experiment: Jev triage, which asks an outside model which checks a task needs and only records the answer. It sends metadata, never code, and is off unless you turn it on.

The same files serve Claude Code, Codex and OpenCode; Host details says what each one adds.

The names

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.

Learn more

The details live in the docs, generated from this repository so they cannot drift from it:

Using it yourself

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.

Credits

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.

Source 2 files
hooks/register.ts 19 lines
1// 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}
19
hooks/fix.ts 156 lines
1// 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