SLOPSHOPPER

sql-guard

Holds psql commands that write (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER...) or run a SQL file, and drizzle-kit push/migrate, until you press Proceed…

panebandguardtoast
★ 21v0.1.0GPL-3.0updated 2026-10-19fcsouza/agent-skills/mods/sql-guard
A shopper browsing a rack in a slop shop
README

Agent Skills

Production-grade skills for AI coding agents (OpenClaw, Claude Code, and others).

Built by Fabricio Cavalcante — Software Engineer & AI Specialist.

Available Plugins

Plugins are complete workflow bundles — skills + agents + hooks working together as an integrated system. Install a plugin to get the full experience.

PluginDescriptionPlatform
game-dev26 skills, 7 commands, 3 agents, 5 hooks — complete game development workflow with engineering, design, narrative, and infrastructureGame Dev

Claude Code Mods

Mods are plugins built on function hooks: they run inside Claude Code, guard tool calls and draw above the prompt. See mods/ for install steps.

ModDescription
commit-guardBlocks commits with machine paths or .env files, git add -A, and pushes to main
prod-guardHolds Dokploy MCP writes until you press Proceed
bun-onlyRewrites npm, npx, yarn and pnpm to bun
done-gateShows edited files and the checks still owed
worktreesShows git worktrees, their changes, and the count
usage-ringsContext and plan usage, coloured by level
pr-statusThe current branch's PR: CI checks and review
sql-guardHolds writes to non-local databases until you press Proceed
pino-guardBlocks pino transports that break bundled logging

Available Skills

Skills are individual, cherry-pickable modules. Install only what you need.

Core

SkillDescriptionPlatform
claude-agent-sdkBuild production AI agents with the Claude Agent SDK — query(), hooks, subagents, custom tools, MCP servers, sessions, permissions, hostingClaude Code / API
database-performance-tuningDiagnose and fix slow databases like a DBA — pg_stat_statements, EXPLAIN plans, index design, query rewrites, vacuum and bloat, safe online rollout. Ships a read-only diagnostic scriptPostgreSQL / MySQL
foundry-vtt-module-devBuild, extend, and maintain Foundry VTT v14 modules — TypeDataModel, ApplicationV2 (detached windows), ActorSheetV2, hooks, sockets, dice, canvas/PIXI, Scene Levels, Regions as templates, ActiveEffects V2, v13→v14 migration, boilerplate filesFoundry VTT
foundry-vtt-system-devBuild Foundry VTT v14 game systems — system.json documentTypes, Actor/Item TypeDataModel, dice mechanics, combat/initiative, ActiveEffect V2 subtypes, data migration, character sheets, status effects, macros, enrichersFoundry VTT
openclaw-genieComprehensive OpenClaw gateway skill — installation, configuration, 38+ channels, memory, tools, hooks, deployment, multi-agentOpenClaw
nuvemshop-sdkPrepare, audit, implement, homologate, and publish Nuvemshop/Tiendanube apps — OAuth, API, NubeSDK, Nexo, Nimbus, webhooks, and App Store approvalNuvemshop
react-animationsReact animation patterns and utilitiesReact

Game Dev — Design

SkillDescription
game-design-fundamentalsCore loops, MDA framework, progression curves
game-economy-designVirtual economies, currencies, loot tables
level-designLevels, environments, difficulty curves
procedural-genDungeon gen, noise terrain, wave function collapse
quest-mission-designQuests, objectives, quest trees
skill-progression-treesSkill trees, talent trees, unlock graphs
ui-ux-gameHUD, menus, onboarding, accessibility

Game Dev — Engineering

SkillDescription
betterauth-integrationAuth, OAuth, JWT, RBAC
bullmq-game-queuesJob queues, async events, scheduled tasks
elevenlabs-sound-musicSFX generation, adaptive music, voice acting
game-backend-architectureGame servers, WebSocket, rooms, tick loops
game-state-syncClient-server sync, rollback netcode, delta compression
gameplay-analyticsRetention analytics, funnels, D1/D7/D30
matchmaking-systemMatchmaking, lobbies, ELO, skill-based queues
postgres-game-schemaGame DB schemas, Drizzle ORM, inventory
redis-game-patternsCaching, leaderboards, pub/sub, rate limiting
stripe-game-paymentsIn-app purchases, subscriptions, webhooks

Game Dev — Infrastructure

SkillDescription
ci-cd-gameCI/CD pipelines, GitHub Actions, deployment
claude-code-game-workflowEntry point for AI agents on game projects
cursor-codex-integrationCursor IDE, .cursorrules, Codex setup
monitoring-game-opsMonitoring, logging, telemetry, alerts

Game Dev — Narrative

SkillDescription
character-design-narrativeCharacters, NPCs, dialogue, archetypes
quest-narrative-coherenceNarrative coherence check, lore validation
story-structure-gameBranching narratives, player agency, story arcs
worldbuildingLore, factions, geography, world bibles

Structure

All skills follow a flat layout:

skills/<skill-name>/
├── README.md           # Overview and installation instructions
├── SKILL.md            # Main skill file (YAML frontmatter + body)
├── boilerplate/        # Starter code to copy and customize (optional)
├── templates/          # Document and config templates (optional)
├── references/         # Deep-dive reference files (optional)
└── ARCHITECTURE.md     # System design decisions (optional)
  • SKILL.md is always the entry point — read this first
  • boilerplate/ files are starter code, copy and customize
  • templates/ are document/config templates to fill in
  • ARCHITECTURE.md explains system design decisions

Installation

game-dev plugin (Claude Code)

/plugin marketplace add fcsouza/agent-skills
/plugin install game-dev@fcsouza-agent-skills

See plugins/game-dev/README.md for the full skill index, commands, and agents.

Individual skills (cherry-pick)

# Install a specific skill
npx skills add fcsouza/agent-skills --skill game-design-fundamentals

# Install all game-dev engineering skills at once
npx skills add fcsouza/agent-skills --skill betterauth-integration --skill postgres-game-schema --skill game-backend-architecture

# List all available skills
npx skills add fcsouza/agent-skills --list

Manual

# Plugin (full bundle)
cp -r plugins/game-dev/ .claude/plugins/game-dev/

# Individual skill
cp -r skills/<skill-name>/ .claude/skills/<skill-name>/

# For OpenClaw
cp -r skills/<skill-name>/ ~/.openclaw/workspace/skills/<skill-name>/

License

GPL-3.0

Source 2 files
hooks/register.tsx 97 lines
1import type { EngineInterface, Register, RenderInput } from 'claude-code';
2
3import { classify, envDatabaseHost, hostsIn, isLocalHost, maskSecrets, type Risk } from './rules';
4
5const PANE_ID = 'sql-guard';
6const POLL_MS = 250;
7const HOLD_LIMIT_MS = 10 * 60 * 1000;
8
9type Held = { risk: Risk; command: string; target: string; decision: 'proceed' | 'cancel' | null; where: 'pane' | 'band' };
10
11let held: Held | null = null;
12
13// The database a command reaches: hosts it names, else DATABASE_URL in the project's .env files.
14async function targetOf($: EngineInterface, command: string): Promise<string[]> {
15  const named = hostsIn(command);
16  if (named.length > 0) return named;
17  const cwd = await $.session.cwd();
18  for (const file of ['.env.local', '.env']) {
19    const text = await $.fs.read(`${cwd}/${file}`).catch(() => null);
20    const host = text === null ? undefined : envDatabaseHost(text);
21    if (host !== undefined) return [host];
22  }
23  return [];
24}
25
26export const register: Register = on => {
27  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
28    const risk = classify(e.command);
29    if (risk === null) return next(e);
30
31    const hosts = await targetOf($, e.command);
32    // Every host local: a development database, nothing to hold. No host found: assume it may be production.
33    if (hosts.length > 0 && hosts.every(isLocalHost)) return next(e);
34
35    while (held !== null) {
36      if (next.signal.aborted) return { deny: `${$.plugin.name}: the turn was interrupted while another database write waited. Nothing ran.` };
37      await $.clock.sleep(POLL_MS);
38    }
39    const mine: Held = {
40      risk,
41      command: maskSecrets(e.command).slice(0, 400),
42      target: hosts.length > 0 ? hosts.join(', ') : 'unknown (no host in the command or .env)',
43      decision: null,
44      where: 'pane',
45    };
46    held = mine;
47
48    let opened = { isPlaced: false };
49    let outcome: 'proceed' | 'cancel' | 'timeout' | 'interrupted' = 'timeout';
50    try {
51      opened = await $.ui.open({ id: PANE_ID, title: 'Database write', focus: true });
52      if (!opened.isPlaced) mine.where = 'band';
53      $.ui.invalidate('ui.render');
54      $.ui.toast(`${$.plugin.name}: ${risk.label} waits for your OK`);
55      const startedAt = await $.clock.now();
56      while (mine.decision === null && !next.signal.aborted && (await $.clock.now()) - startedAt <= HOLD_LIMIT_MS) {
57        await $.clock.sleep(POLL_MS);
58      }
59      outcome = mine.decision ?? (next.signal.aborted ? 'interrupted' : 'timeout');
60    } finally {
61      if (opened.isPlaced) await $.ui.close({ id: PANE_ID }).catch(() => undefined);
62      if (held === mine) held = null;
63      $.ui.invalidate('ui.render');
64    }
65
66    if (outcome === 'proceed') return next(e);
67    const why = { cancel: 'the user pressed Cancel', timeout: 'no answer within 10 minutes', interrupted: 'the turn was interrupted' }[outcome];
68    return { deny: `${$.plugin.name}: the ${risk.label} against ${mine.target} did not run: ${why}. Do not retry it unless the user asks.` };
69  }).catch(($, e, next) => (next.called ? next(e) : { deny: `${$.plugin.name}: the guard failed, so the database command did not run.` }));
70
71  on('ui.render', { component: 'Pane', requestId: PANE_ID }, ($, e, next) => (held === null ? next(e) : draw($, e, held)));
72  on('ui.render', { component: 'AbovePrompt' }, ($, e, next) => (held === null || held.where !== 'band' ? next(e) : draw($, e, held)));
73};
74
75function draw($: EngineInterface, e: RenderInput<'Pane' | 'AbovePrompt'>, call: Held) {
76  const { Box, Button, Text } = $.ui.resolve(e);
77  const decide = (decision: 'proceed' | 'cancel') => () => {
78    call.decision = decision;
79    $.ui.invalidate('ui.render');
80  };
81  return (
82    <Box flexDirection="column" borderStyle="round" borderColor="#ef4444" paddingX={1}>
83      <Text bold color="#ef4444">
84        Database write: {call.risk.label}
85      </Text>
86      <Text>Target: {call.target}</Text>
87      <Text dimColor wrap="wrap">
88        {call.command}
89      </Text>
90      <Box gap={1} marginTop={1}>
91        <Button key="proceed" label="Proceed" onPress={decide('proceed')} />
92        <Button key="cancel" label="Cancel" onPress={decide('cancel')} />
93      </Box>
94    </Box>
95  );
96}
97
hooks/rules.ts 51 lines
1// Which shell commands write to a database, and whether that database is local. Pure, so the tests call it directly.
2
3export type Risk = { kind: 'sql-write' | 'sql-file' | 'migration'; label: string };
4
5const PSQL = /(^|[\s/;&|(])psql(\s|$)/;
6const WRITE_SQL =
7  /\b(insert\s+into|update\s+\w|delete\s+from|drop\s+(table|schema|database|index|view|column|type)|truncate\b|alter\s+(table|schema|database|type)|create\s+(table|schema|database|index|unique|type)|grant\b|revoke\b)/i;
8const SQL_FILE = /\s(-f|--file)(\s|=)|<\s*\S+\.sql\b|<<\s*['"]?\w+/;
9const MIGRATION = /\b(drizzle-kit\s+(push|migrate|drop)|bun\s+run\s+(db:)?(push|migrate)\b|bun\s+run\s+db:(reset|seed)\b)/;
10
11export function classify(command: string): Risk | null {
12  if (MIGRATION.test(command)) {
13    return { kind: 'migration', label: 'schema migration' };
14  }
15  if (!PSQL.test(command)) {
16    return null;
17  }
18  const write = WRITE_SQL.exec(command);
19  if (write !== null) {
20    return { kind: 'sql-write', label: write[0].replace(/\s+/g, ' ').toUpperCase() };
21  }
22  if (SQL_FILE.test(command)) {
23    return { kind: 'sql-file', label: 'SQL from a file or heredoc' };
24  }
25  return null;
26}
27
28// A host with no dot (`db`, `postgres`) is a Docker Compose service name: a managed database always has a domain.
29const LOCAL_HOST = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?|0\.0\.0\.0|host\.docker\.internal|[\w-]+\.localhost|[a-z][\w-]*)$/i;
30
31export const isLocalHost = (host: string): boolean => LOCAL_HOST.test(host);
32
33/** Database hosts named in a command: postgres URLs, psql -h/--host, PGHOST=. */
34export function hostsIn(command: string): string[] {
35  const hosts: string[] = [];
36  for (const match of command.matchAll(/postgres(?:ql)?:\/\/(?:[^@\s/'"]*@)?(\[[^\]]+\]|[^:/\s?'"]+)/g)) hosts.push(match[1] ?? '');
37  for (const match of command.matchAll(/(?:\s-h\s*|--host[=\s])['"]?([\w.:-]+)/g)) hosts.push(match[1] ?? '');
38  for (const match of command.matchAll(/\bPGHOST=['"]?([\w.:-]+)/g)) hosts.push(match[1] ?? '');
39  return hosts.filter(host => host !== '');
40}
41
42/** The host in a DATABASE_URL line of a .env file, if any. */
43export function envDatabaseHost(envFile: string): string | undefined {
44  const line = envFile.split('\n').find(row => /^\s*(export\s+)?DATABASE_URL\s*=/.test(row));
45  return line === undefined ? undefined : hostsIn(line)[0];
46}
47
48/** A command for display, with any URL password hidden. */
49export const maskSecrets = (command: string): string =>
50  command.replace(/(postgres(?:ql)?:\/\/[^:@\s/]+):[^@\s]+@/g, '$1:***@').replace(/\bPGPASSWORD=\S+/g, 'PGPASSWORD=***');
51