SLOPSHOPPER

corgi-guard

Stops the Bash shapes that fail in corgi runs (zsh set -- $pair, rm -rf on corgi worktrees) and adds the corgi fix to errors it knows

newguard
★ 37v0.1.1MITupdated 2026-10-06Andriiklymiuk/corgi/plugins/corgi-guard
A shopper browsing a rack in a slop shop
README

corgi-guard

A Claude Code mod that stops the Bash shapes corgi runs lose turns to, and hands the model the corgi fix:

  • set -- $pair - zsh does not split it, so the second word arrives empty. Refused, with read -r a b <<< "$pair".
  • rm -rf /tmp/corgi-wt/... or /tmp/corgi-review/... - refused in a hardened workspace anyway. Refused, with the fresh-path recipe.
  • A GraphQL call with an empty variable - the result carries a pointer to corgi agent watch pr threads.
  • gh pr checks right after a push ("no checks reported") - the result carries the wait-for-checks loop.
  • A corgi chat post with a dead Slack token - the result says to tell you and how to renew it.

Install

/plugin install corgi-guard --marketplace Andriiklymiuk/corgi
Source 2 files
hooks/register.ts 22 lines
1import type { Register } from 'claude-code'
2
3import { hint, refusal } from './rules'
4
5export const register: Register = on => {
6  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
7    const why = refusal(e.command)
8    if (why !== undefined) {
9      return { deny: `corgi-guard: ${why}` }
10    }
11    const ran = await next(e)
12    if (ran.deny !== undefined) {
13      return ran
14    }
15    const extra = hint(e.command, ran.text ?? '')
16    if (extra === undefined) {
17      return ran
18    }
19    return { ...ran, context: [...(ran.context ?? []), `corgi-guard: ${extra}`] }
20  }).catch(($, e, next) => next(e))
21}
22
hooks/rules.ts 39 lines
1const unsplitSet = /\bset\s+--\s+"?\$(?!\{=)[A-Za-z_]\w*/
2const rmCorgiTree = /\brm\s+-(?:[a-zA-Z]*r[a-zA-Z]*f|[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\s+[^;&|]*\/tmp\/corgi-(?:wt|review)\b/
3
4const heredocBody = /<<-?\s*(['"]?)(\w+)\1([^\n]*)\n[\s\S]*?\n\s*\2[ \t]*(?=\n|$)/g
5const singleQuoted = /'[^']*'/g
6
7export function shellOnly(command: string): string {
8  return command.replace(heredocBody, '<<$2$3').replace(singleQuoted, "''")
9}
10
11export function refusal(raw: string): string | undefined {
12  const command = shellOnly(raw)
13  if (unsplitSet.test(command)) {
14    return 'zsh does not split `$var` in `set -- $var`, so the second word arrives empty. ' +
15      'Use `read -r repo n <<< "$pair"` (bash and zsh), or loop over `repo:n` and take `${p%%:*}` / `${p##*:}`.'
16  }
17  if (rmCorgiTree.test(command)) {
18    return '`rm -rf` is refused in a hardened workspace. Take a fresh path instead: ' +
19      '`W=/tmp/corgi-wt/<id>; [ -e "$W" ] && W="$W-$(date +%s)"`, and `git -C <repo> worktree remove "$W"` (no --force) when done.'
20  }
21  return undefined
22}
23
24export function hint(command: string, text: string): string | undefined {
25  if (/graphql/.test(command) && /Could not coerce value|provided invalid value/.test(text)) {
26    return 'A GraphQL variable arrived empty. For review threads, `corgi agent watch pr threads <url> <url>...` ' +
27      'reads every open thread of every PR/MR with the thread id and reply-to id, GitHub or GitLab, in one call.'
28  }
29  if (/\bgh\s+pr\s+checks\b/.test(command) && /no checks reported/.test(text)) {
30    return 'The checks are not registered yet right after a push. Wait for them inside one background command: ' +
31      '`until [ "$(gh pr checks <n> --json name -q length 2>/dev/null)" -gt 0 ] 2>/dev/null; do sleep 10; done; gh pr checks <n> --watch --fail-fast`.'
32  }
33  if (/\bcorgi\b.*\bchat\b/.test(command) && /token_revoked|invalid_auth|account_inactive/.test(text)) {
34    return 'The stored Slack token is dead, so nothing was posted. Tell the user in one line, with the text to paste themselves, ' +
35      'and the fix: `corgi agent watch auth slack --token <new token>` (`corgi agent doctor` names which token).'
36  }
37  return undefined
38}
39