Blocks commits that carry absolute paths from this machine, git add -A / git add ., and pushes straight to main or master.

Production-grade skills for AI coding agents (OpenClaw, Claude Code, and others).
Built by Fabricio Cavalcante — Software Engineer & AI Specialist.
Plugins are complete workflow bundles — skills + agents + hooks working together as an integrated system. Install a plugin to get the full experience.
| Plugin | Description | Platform |
|---|---|---|
| game-dev | 26 skills, 7 commands, 3 agents, 5 hooks — complete game development workflow with engineering, design, narrative, and infrastructure | Game Dev |
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.
| Mod | Description |
|---|---|
| commit-guard | Blocks commits with machine paths or .env files, git add -A, and pushes to main |
| prod-guard | Holds Dokploy MCP writes until you press Proceed |
| bun-only | Rewrites npm, npx, yarn and pnpm to bun |
| done-gate | Shows edited files and the checks still owed |
| worktrees | Shows git worktrees, their changes, and the count |
| usage-rings | Context and plan usage, coloured by level |
| pr-status | The current branch's PR: CI checks and review |
| sql-guard | Holds writes to non-local databases until you press Proceed |
| pino-guard | Blocks pino transports that break bundled logging |
Skills are individual, cherry-pickable modules. Install only what you need.
| Skill | Description | Platform |
|---|---|---|
| claude-agent-sdk | Build production AI agents with the Claude Agent SDK — query(), hooks, subagents, custom tools, MCP servers, sessions, permissions, hosting | Claude Code / API |
| database-performance-tuning | Diagnose 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 script | PostgreSQL / MySQL |
| foundry-vtt-module-dev | Build, 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 files | Foundry VTT |
| foundry-vtt-system-dev | Build 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, enrichers | Foundry VTT |
| openclaw-genie | Comprehensive OpenClaw gateway skill — installation, configuration, 38+ channels, memory, tools, hooks, deployment, multi-agent | OpenClaw |
| nuvemshop-sdk | Prepare, audit, implement, homologate, and publish Nuvemshop/Tiendanube apps — OAuth, API, NubeSDK, Nexo, Nimbus, webhooks, and App Store approval | Nuvemshop |
| react-animations | React animation patterns and utilities | React |
| Skill | Description |
|---|---|
| game-design-fundamentals | Core loops, MDA framework, progression curves |
| game-economy-design | Virtual economies, currencies, loot tables |
| level-design | Levels, environments, difficulty curves |
| procedural-gen | Dungeon gen, noise terrain, wave function collapse |
| quest-mission-design | Quests, objectives, quest trees |
| skill-progression-trees | Skill trees, talent trees, unlock graphs |
| ui-ux-game | HUD, menus, onboarding, accessibility |
| Skill | Description |
|---|---|
| betterauth-integration | Auth, OAuth, JWT, RBAC |
| bullmq-game-queues | Job queues, async events, scheduled tasks |
| elevenlabs-sound-music | SFX generation, adaptive music, voice acting |
| game-backend-architecture | Game servers, WebSocket, rooms, tick loops |
| game-state-sync | Client-server sync, rollback netcode, delta compression |
| gameplay-analytics | Retention analytics, funnels, D1/D7/D30 |
| matchmaking-system | Matchmaking, lobbies, ELO, skill-based queues |
| postgres-game-schema | Game DB schemas, Drizzle ORM, inventory |
| redis-game-patterns | Caching, leaderboards, pub/sub, rate limiting |
| stripe-game-payments | In-app purchases, subscriptions, webhooks |
| Skill | Description |
|---|---|
| ci-cd-game | CI/CD pipelines, GitHub Actions, deployment |
| claude-code-game-workflow | Entry point for AI agents on game projects |
| cursor-codex-integration | Cursor IDE, .cursorrules, Codex setup |
| monitoring-game-ops | Monitoring, logging, telemetry, alerts |
| Skill | Description |
|---|---|
| character-design-narrative | Characters, NPCs, dialogue, archetypes |
| quest-narrative-coherence | Narrative coherence check, lore validation |
| story-structure-game | Branching narratives, player agency, story arcs |
| worldbuilding | Lore, factions, geography, world bibles |
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 firstboilerplate/ files are starter code, copy and customizetemplates/ are document/config templates to fill inARCHITECTURE.md explains system design decisions/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.
# 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
# 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>/
GPL-3.0
hooks/register.ts 75 lines1import type { EngineInterface, Register } from 'claude-code';
2
3import { addsEverything, envFilesIn, findLeaks, pushTargets, resolveDir, splitCommands, stagesTracked } from './rules';
4
5const PROTECTED_BRANCHES = new Set(['main', 'master']);
6
7function git($: EngineInterface, cwd: string, args: readonly string[]) {
8 return $.process.run(['git', '--no-optional-locks', '-C', cwd, ...args], { timeoutMs: 10_000 });
9}
10
11export const register: Register = on => {
12 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
13 const commands = splitCommands(e.command).filter(command => command.git !== null);
14 if (commands.length === 0) {
15 return next(e);
16 }
17 const sessionCwd = await $.session.cwd();
18
19 for (const command of commands) {
20 if (command.git === null) {
21 continue;
22 }
23 const cwd = resolveDir(sessionCwd, command.dir);
24 const { verb, args } = command.git;
25
26 if (verb === 'add' && addsEverything(args)) {
27 return {
28 deny: `${$.plugin.name}: \`git add ${args.join(' ')}\` stages every file, unread. Stage the files by name, then read \`git status --short\` and \`git diff --cached\` before committing.`,
29 };
30 }
31
32 if (verb === 'add') {
33 const env = envFilesIn(args.filter(arg => !arg.startsWith('-')));
34 if (env.length > 0) {
35 return { deny: `${$.plugin.name}: \`${env.join(' ')}\` holds secrets and never goes into git. Leave it out, and check it is in .gitignore.` };
36 }
37 }
38
39 if (verb === 'commit') {
40 const base = stagesTracked(args) ? ['diff', 'HEAD'] : ['diff', '--cached'];
41 const names = await git($, cwd, [...base, '--name-only']);
42 const env = envFilesIn(names.stdout.split('\n'));
43 if (env.length > 0) {
44 return {
45 deny: `${$.plugin.name}: the commit would include ${env.map(file => `\`${file}\``).join(', ')}, which hold secrets. Unstage with \`git restore --staged ${env.join(' ')}\` and add them to .gitignore.`,
46 };
47 }
48 const diff = await git($, cwd, base);
49 const leaks = findLeaks(diff.stdout);
50 if (leaks.length > 0) {
51 const shown = leaks
52 .slice(0, 5)
53 .map(leak => ` ${leak.file}: ${leak.line}`)
54 .join('\n');
55 return {
56 deny: `${$.plugin.name}: the staged diff holds an absolute path from this machine (${leaks.length} line${leaks.length === 1 ? '' : 's'}):\n${shown}\nRemove the path, or leave that file out of the commit. A lockfile written by a local link needs a fresh install, not a version edit.`,
57 };
58 }
59 }
60
61 if (verb === 'push') {
62 const head = await git($, cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
63 const hit = pushTargets(args, head.stdout.trim()).find(target => PROTECTED_BRANCHES.has(target));
64 if (hit !== undefined) {
65 return {
66 deny: `${$.plugin.name}: this pushes straight to \`${hit}\`. Push a branch and open a PR instead; only the user pushes to ${hit}.`,
67 };
68 }
69 }
70 }
71
72 return next(e);
73 }).catch(($, e, next) => (next.called ? next(e) : { deny: `${$.plugin.name}: the guard failed, so the command did not run.` }));
74};
75hooks/rules.ts 174 lines1// Pure parsing for commit-guard: no `$`, so the tests can call it directly.
2
3export type GitCall = { verb: string; args: string[] };
4export type Command = { dir: string | null; git: GitCall | null };
5export type Leak = { file: string; line: string };
6
7// A path from this machine: a home folder, or a pnpm/npm link or file reference.
8// Joined from pieces so this file never matches its own pattern in a diff.
9export const LEAK_PARTS = ['/' + 'Users/', '/' + 'home/', 'link' + ':/', 'file' + ':/'];
10const LEAK = new RegExp(LEAK_PARTS.map(part => part.replaceAll('/', '\\/')).join('|'));
11
12// Options of `git` itself that take a value: `git -c k=v commit` skips both words.
13const GIT_VALUE_OPTIONS = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace']);
14
15/** Splits a shell line into words, keeping quoted runs together. Good enough for git commands. */
16export function words(text: string): string[] {
17 const out: string[] = [];
18 const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
19 for (const match of text.matchAll(re)) {
20 out.push(match[1] ?? match[2] ?? match[3] ?? '');
21 }
22 return out;
23}
24
25/**
26 * Splits a command line on `&&`, `||`, `;` and `|`, and for each part reports the
27 * folder a leading `cd dir` moved to and the git call it makes, if any.
28 */
29export function splitCommands(line: string): Command[] {
30 const commands: Command[] = [];
31 let dir: string | null = null;
32 for (const part of splitOutsideQuotes(line)) {
33 const ws = words(part.trim());
34 if (ws[0] === 'cd' && ws[1] !== undefined) {
35 dir = join(dir, ws[1]);
36 continue;
37 }
38 commands.push(parseGit(ws, dir));
39 }
40 return commands;
41}
42
43/** Splits on `&&`, `||`, `;`, `|` and newlines that sit outside quotes. */
44function splitOutsideQuotes(line: string): string[] {
45 const parts: string[] = [];
46 let current = '';
47 let quote: string | null = null;
48 for (let i = 0; i < line.length; i += 1) {
49 const char = line[i] ?? '';
50 if (quote !== null) {
51 current += char;
52 if (char === '\\' && quote === '"') {
53 current += line[i + 1] ?? '';
54 i += 1;
55 } else if (char === quote) {
56 quote = null;
57 }
58 continue;
59 }
60 if (char === '"' || char === "'") {
61 quote = char;
62 current += char;
63 continue;
64 }
65 const pair = line.slice(i, i + 2);
66 if (pair === '&&' || pair === '||') {
67 parts.push(current);
68 current = '';
69 i += 1;
70 continue;
71 }
72 if (char === ';' || char === '|' || char === '\n') {
73 parts.push(current);
74 current = '';
75 continue;
76 }
77 current += char;
78 }
79 parts.push(current);
80 return parts;
81}
82
83function join(dir: string | null, next: string): string {
84 return dir === null || next.startsWith('/') || next.startsWith('~') ? next : `${dir}/${next}`;
85}
86
87function parseGit(ws: string[], startDir: string | null): Command {
88 let dir = startDir;
89 const none = { dir, git: null };
90 // Skip env assignments (`FOO=1 git ...`) and wrappers like `rtk`.
91 let i = 0;
92 while (i < ws.length && (/^[A-Z_][A-Z0-9_]*=/.test(ws[i] ?? '') || ws[i] === 'rtk' || ws[i] === 'command')) {
93 i += 1;
94 }
95 if (ws[i] !== 'git') {
96 return none;
97 }
98 i += 1;
99 while (i < ws.length && (ws[i] ?? '').startsWith('-')) {
100 const option = ws[i] ?? '';
101 const value = ws[i + 1];
102 if (option === '-C' && value !== undefined) {
103 dir = join(dir, value);
104 }
105 i += GIT_VALUE_OPTIONS.has(option) ? 2 : 1;
106 }
107 const verb = ws[i];
108 return verb === undefined ? { dir, git: null } : { dir, git: { verb, args: ws.slice(i + 1) } };
109}
110
111/** The folder a command runs in: the session folder, moved by `cd` or `git -C`. */
112export function resolveDir(sessionCwd: string, dir: string | null): string {
113 if (dir === null) {
114 return sessionCwd;
115 }
116 return dir.startsWith('/') ? dir : `${sessionCwd}/${dir}`;
117}
118
119/** `git add -A`, `--all`, `.`, `:/` and `*` stage what nobody read. */
120export function addsEverything(args: readonly string[]): boolean {
121 return args.some(arg => arg === '-A' || arg === '--all' || arg === '.' || arg === ':/' || arg === '*' || /^-[a-zA-Z]*A[a-zA-Z]*$/.test(arg));
122}
123
124/** `git commit -a` / `-am` / `--all` stages tracked changes at commit time. */
125export function stagesTracked(args: readonly string[]): boolean {
126 return args.some(arg => arg === '--all' || /^-[a-zA-Z]*a[a-zA-Z]*$/.test(arg));
127}
128
129/** Added lines of a unified diff that hold a path from this machine. */
130export function findLeaks(diff: string): Leak[] {
131 const leaks: Leak[] = [];
132 let file = '?';
133 for (const line of diff.split('\n')) {
134 if (line.startsWith('+++ ')) {
135 file = line.slice(4).replace(/^b\//, '');
136 continue;
137 }
138 if (line.startsWith('+') && LEAK.test(line)) {
139 leaks.push({ file, line: line.slice(1).trim().slice(0, 160) });
140 }
141 }
142 return leaks;
143}
144
145/**
146 * The branches a `git push` would update: each refspec's destination, or the
147 * current branch when the push names none.
148 */
149export function pushTargets(args: readonly string[], currentBranch: string): string[] {
150 // --all and --mirror push every local branch, main included.
151 if (args.some(arg => arg === '--all' || arg === '--mirror' || arg === '--branches')) {
152 return ['main', 'master'];
153 }
154 const positional = args.filter(arg => !arg.startsWith('-'));
155 const refspecs = positional.slice(1);
156 if (refspecs.length === 0) {
157 return [currentBranch];
158 }
159 return refspecs.map(spec => {
160 const destination = spec.includes(':') ? spec.slice(spec.indexOf(':') + 1) : spec;
161 const name = destination.replace(/^\+/, '').replace(/^refs\/heads\//, '');
162 return name === 'HEAD' ? currentBranch : name;
163 });
164}
165
166// `.env`, `.env.local`, `.env.production`, `foo/.env`... but not the templates meant to be shared.
167const ENV_FILE = /(^|\/)\.env(\.[\w.-]+)?$/;
168const ENV_TEMPLATE = /\.(example|sample|template|dist)$/;
169
170export const isEnvFile = (path: string): boolean => ENV_FILE.test(path) && !ENV_TEMPLATE.test(path);
171
172/** The .env files among `git diff --name-only` output, or among `git add` paths. */
173export const envFilesIn = (paths: readonly string[]): string[] => paths.map(path => path.trim()).filter(path => path !== '' && isEnvFile(path));
174