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

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.corgi agent watch pr threads.gh pr checks right after a push ("no checks reported") - the result carries the wait-for-checks loop./plugin install corgi-guard --marketplace Andriiklymiuk/corgihooks/register.ts 22 lines1import 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}
22hooks/rules.ts 39 lines1const 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