SLOPSHOPPER

guardia-regole

Blocca commit e push su main/master e i segreti in chiaro nei file scritti da Claude

newguardprocess
v0.1.0no licenseupdated 2026-10-14StraordinaryAS/claude-mods/mods/guardia-regole
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · guardia-regole
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(rm -rf build && git push --force origin main) ⎿ Denied by guardia-regole: guardia-regole: push verso main/master bloccato. Regola di Ale: mai lavorare su ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

I mods di Ale per Claude Code (terminale, scheda Code del desktop, VS Code).

Installazione

Una volta sola, nel terminale:

claude plugin marketplace add StraordinaryAS/claude-mods
claude plugin install guardia-regole@ale-mods
claude plugin install scelta-modello@ale-mods
claude plugin install tipografia-it@ale-mods

Dentro una sessione Claude Code: /plugin install guardia-regole --marketplace StraordinaryAS/claude-mods

App desktop senza terminale: + accanto al prompt › Plugins › Add marketplace › StraordinaryAS/claude-mods › Add plugin.

Aggiornare dopo nuove versioni: claude plugin marketplace update ale-mods, poi /reload-plugins nella sessione.

Mods

ModCosa fa
guardia-regoleBlocca commit e push su main/master e i segreti in chiaro (token, chiavi, password) nei file scritti da Claude. I file .env sono ammessi.
scelta-modelloPer ogni messaggio sceglie Haiku (traduzioni, riassunti, formattazione), Opus (piani, architettura, bug, revisioni) o Sonnet (tutto il resto). Le conferme ("vai", "ok", "2") tengono il modello di prima. @haiku, @sonnet, @opus a inizio messaggio forzano la scelta. A conversazione lunga non scende di livello, per non perdere la cache. /modello-auto off lo spegne. Regole in mods/scelta-modello/hooks/rules.ts.
tipografia-itSkill con le regole di tipografia italiana per documenti, PDF e web: righe isolate, spazi, trattini, caporali, apostrofo ’, accenti, font e interlinea.

Skill nella chat claude.ai

I plugin non arrivano nella chat di claude.ai. Per avere tipografia-it anche lì, comprimi la cartella mods/tipografia-it/skills/tipografia-it in uno zip e caricalo da Impostazioni › Funzionalità › Skill › Carica skill.

Aggiungere un mod

Nuova cartella mods/<nome>/ con .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.ts e test in tests/. Poi una voce in .claude-plugin/marketplace.json. Controllo: claude plugin validate mods/<nome> e claude plugin test mods/<nome>.

Source 3 files
hooks/register.ts 38 lines
1import type { Register } from 'claude-code'
2import { findSecret, isEnvFile } from './secrets'
3import { cdBefore, gitActions, isProtected, pushHasRefspec, pushNamesProtected } from './git'
4
5const NO_MAIN =
6  'Regola di Ale: mai lavorare su main. Crea un ramo (git switch -c <nome>) e fai lì commit e push; il merge su main solo dopo il suo via.'
7
8export const register: Register = on => {
9  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
10    const command = e.command
11    for (const { verb, dir, args } of gitActions(command)) {
12      if (verb === 'push' && pushNamesProtected(args))
13        return { deny: `${$.plugin.name}: push verso main/master bloccato. ${NO_MAIN}` }
14      if (verb === 'push' && pushHasRefspec(args)) continue
15
16      const cwd = dir ?? cdBefore(command, command.indexOf('git'))
17      const head = await $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd ? { cwd } : undefined)
18      if (head.exitCode === 0 && isProtected(head.stdout))
19        return { deny: `${$.plugin.name}: ${verb} sul ramo ${head.stdout.trim()} bloccato. ${NO_MAIN}` }
20    }
21    return next(e)
22  }).catch(($, e, next) => next(e)) // se la guardia si rompe, non blocca il lavoro
23
24  on('tool.call', { tool: ['Write', 'Edit', 'NotebookEdit'] }, ($, e, next) => {
25    const path = 'file_path' in e ? e.file_path : 'notebook_path' in e ? e.notebook_path : ''
26    if (isEnvFile(String(path))) return next(e)
27    const text = 'content' in e ? e.content : 'new_string' in e ? e.new_string : 'new_source' in e ? e.new_source : ''
28    const kind = findSecret(String(text ?? ''))
29    return kind
30      ? {
31          deny:
32            `${$.plugin.name}: ${kind} in chiaro in ${path}. Regola di Ale: segreti mai in chiaro nei file. ` +
33            'Mettilo in .env (non versionato) o in una variabile d\'ambiente e leggilo da lì.',
34        }
35      : next(e)
36  }).catch(($, e, next) => next(e))
37}
38
hooks/secrets.ts 28 lines
1// Pattern dei segreti più comuni. Un match blocca la scrittura del file.
2const PATTERNS: ReadonlyArray<readonly [string, RegExp]> = [
3  ['chiave privata', /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
4  ['token GitHub', /\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{40,})\b/],
5  ['chiave Anthropic', /\bsk-ant-[A-Za-z0-9_-]{20,}/],
6  ['chiave OpenAI', /\bsk-(proj-)?[A-Za-z0-9_-]{32,}/],
7  ['token Supabase', /\b(sbp_[a-f0-9]{40}|sb_secret_[A-Za-z0-9_-]{20,})\b/],
8  ['JWT (es. chiave Supabase)', /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/],
9  ['chiave AWS', /\bAKIA[0-9A-Z]{16}\b/],
10  ['chiave Stripe live', /\b(sk|rk)_live_[A-Za-z0-9]{20,}/],
11  ['token Slack', /\bxox[abprs]-[A-Za-z0-9-]{10,}/],
12  ['chiave Google API', /\bAIza[0-9A-Za-z_-]{35}\b/],
13  [
14    'password o token assegnato in chiaro',
15    /\b(password|passwd|secret|token|api[_-]?key|access[_-]?key)\b["']?\s*[:=]\s*["'](?![^"']*(\$\{|process\.env|<|your|xxx|example))[^"'\s]{12,}["']/i,
16  ],
17]
18
19/** Primo tipo di segreto trovato nel testo, o undefined. */
20export const findSecret = (text: string): string | undefined =>
21  PATTERNS.find(([, re]) => re.test(text))?.[0]
22
23/** I file .env locali sono il posto giusto per i segreti (non vanno su git). */
24export const isEnvFile = (path: string): boolean => {
25  const name = path.split(/[\\/]/).pop() ?? ''
26  return /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template)$/.test(name)
27}
28
hooks/git.ts 41 lines
1const PROTECTED = new Set(['main', 'master'])
2
3/** I comandi git del testo che fanno commit o push, uno per segmento. */
4export const gitActions = (command: string) =>
5  command
6    .split(/&&|\|\||;|\n|\|/)
7    .map(s => s.trim())
8    .flatMap(segment => {
9      const m = segment.match(/^(?:\S+=\S+\s+)*git\b(.*)$/)
10      if (!m) return []
11      const tokens = (m[1] ?? '').trim().split(/\s+/).filter(Boolean)
12      let dir: string | undefined
13      const rest: string[] = []
14      for (let i = 0; i < tokens.length; i++) {
15        if (tokens[i] === '-C') dir = tokens[++i]
16        else rest.push(tokens[i]!)
17      }
18      const at = rest.findIndex(t => !t.startsWith('-'))
19      const verb = rest[at]
20      if (verb !== 'commit' && verb !== 'push') return []
21      const args = rest.slice(at + 1)
22      return [{ verb, dir, args } as const]
23    })
24
25/** La cartella dell'ultimo `cd` prima del comando git, se c'è. */
26export const cdBefore = (command: string, git: number): string | undefined => {
27  const before = command.slice(0, git)
28  const all = [...before.matchAll(/(?:^|&&|;)\s*cd\s+("[^"]+"|'[^']+'|\S+)/g)]
29  return all.at(-1)?.[1]?.replace(/^["']|["']$/g, '')
30}
31
32/** Il push nomina esplicitamente main/master (es. `origin main`, `HEAD:main`). */
33export const pushNamesProtected = (args: readonly string[]) =>
34  args.some(a => !a.startsWith('-') && PROTECTED.has(a.split(':').pop()!.replace(/^refs\/heads\//, '')))
35
36/** Il push indica un ramo esplicito (remote + refspec). */
37export const pushHasRefspec = (args: readonly string[]) =>
38  args.filter(a => !a.startsWith('-')).length >= 2
39
40export const isProtected = (branch: string) => PROTECTED.has(branch.trim())
41