Denies edits to secret files, lockfiles, .git and node_modules, and destructive Bash or PowerShell commands (recursive deletes of root-like paths, force push…

Agent skills, optional MCP tools, and layered coding standards for AI-assisted development — Claude Code, OpenCode, Codex, and any coding agent that reads markdown.
Portable SKILL.md agent skills, framework-agnostic coding standards, and opt-in free/local MCP servers (UI prototyping, image reading, image generation, file pre-filtering) for teams building with AI pair programmers. One clone. Drop two folders into any project. Every session works your way — and gets better at it with use.
Listed under AI Hub → Skills on Three.js Resources — a curated directory for Three.js AI tools.
git clone https://github.com/AftabIbrahimKazi/ai-dev-kit.git
cp -r ai-dev-kit/skills ai-dev-kit/coding-standards your-project/
On Windows PowerShell: Copy-Item -Recurse ai-dev-kit/skills, ai-dev-kit/coding-standards your-project/
Then open Claude Code, OpenCode, Codex or any agent that reads markdown in your-project and say:
read skills/README.md and install
The installer copies markdown files only: no packages, no build step. Full details: Install into a project.
Why use it
code-audit counts violations across a whole project and lists the exact lines, so the agent reads only what is flagged.The kit is two independent systems that share one install path. skills/ teaches an AI coding agent how to work — session discipline, model-specific behavior, memory, coordination. coding-standards/ teaches it what correct code looks like for this project — language rules, file-role conventions, framework overrides. Either can be adopted alone; together, the coding-standards skill is what loads and enforces the standards chain during a session, so a project that wants enforcement needs both folders.
Every skill is a single portable markdown file with name + description frontmatter — the description is trigger-rich ("trigger when…") so it loads only when relevant — and reads a project-local, gitignored learnings.md sidecar at start and appends a lesson at end, so this repo stays the clean upstream while each install compounds its own experience. Every standard is testable and declarative, with wrong/right examples wherever a rule could be misread, and flags a gap (RULE AI-12) rather than inventing a rule where the standards are silent. They interlock at one seam: the install-kit skill installs both in one pass, and the coding-standards skill is what actually loads and walks the standards chain during a session — without it, coding-standards/ is just reference documentation.
skills/ — the self-improving skills libraryFull catalog: skills/README.md
| Category | Covers | Representative skills |
|---|---|---|
models/claude/ | Protocols tuned to each Claude model's actual behavior | fable-5-1, opus-5-5, sonnet-5-5, haiku-5-5 (current) plus fable-5, opus-5, opus-4-8, sonnet-5, haiku-4-5 (previous gen), claude-all-models (fleet routing), opus-as-fable, hooks-enforcement (opt-in Claude Code hooks) |
models/opencode/ | Open-weight fleet driven through OpenCode | opencode-all-models (routing), GLM, DeepSeek, Kimi, Qwen3-Coder, MiniMax, Devstral, MiMo, gpt-oss, local-small-models (≤32B self-hosted) |
workflow/ | Session/process discipline, model-independent | intent-capture → plan-first → interpretation-checkpoint → pre-merge-gate / pre-commit pipeline; plus handover, debug-protocol, session-budget, perf-audit, role-session (parallel sessions), e2e-scaffold |
standards/ | Loads and enforces the coding-standards chain; audits a whole project against the script-checkable rules; sets comment volume | coding-standards, code-audit, comment-style (none / terse / descriptive — token-saving, asked at install) |
memory/ | Persistent knowledge across sessions | memory-bank (repo-committed context/decisions), memory-gardener (prunes/merges learnings) |
stack/ | Technology-specific discipline | threejs-scene (shaders, disposal, render hygiene), astro-page (convention-driven scaffolding) |
libraries/ | The author's own libraries | strata-css, triforge |
meta/ | Maintains the library itself | skill-writer (quality bar), install-kit (installer), skill-scope (pinned/archivable/addon classification), mod-writer and mod-setup (Claude Code mods) |
addons/ | Optional, user-opted capabilities — never installed by default | system1-prefilter (typed-decision prefiltering; hosted endpoint or the free local Laya model, exposed as the prefilter MCP tool) |
mcp/ | Optional MCP servers behind one meta skill — never installed by default | mcp-manager (setup interview, global-first per-tool config, MCP-first-then-fallback) + one companion per server: stitch, local-vision, image-gen |
coding-standards/ — the layered standards systemFull map: coding-standards/index.md
| Layer | What it is | Location |
|---|---|---|
| 1 — Universal global rules | One rule file per discipline, applies to every file of that type | css-standards.md, html-standards.md, and one script standard (js-, ts-, or js-and-ts-standards.md — never more than one per project) |
| 2 — Universal file-role rules | Partials for each file role within a discipline (e.g. a CSS token file vs. an overlay file; a script's entry vs. orchestrator vs. controller file) | matching {discipline}-standards/ subfolder |
| 3 — Framework rules | Extends or explicitly overrides a universal rule (OVERRIDES [file] RULE [n]) | frameworks/ — currently Astro, Bootstrap, Strata CSS |
| Cross-cutting | Covers |
|---|---|
git-standards.md, versioning-standards.md | Commit/branch/PR conventions, package versioning |
seo-standards.md, accessibility-standards.md | SEO structure/schema; WCAG 2.1 AA |
qa/ (umbrella folder, not one file) | Definition of done, branch gates, logic/error checks, security, E2E testing, bug reporting |
ai-standards.md | The AI behavioral contract — hallucination detection, [CX] context-integrity signal, read-efficiency rules — for every session regardless of tool |
tooling/ | Lint configs that mechanically enforce whichever rules above are machine-checkable |
One setup interview (mcp-manager) wires any of these into Claude Code, OpenCode, or another MCP client. Models, runtimes, usage ledgers and keys live once in ~/.ai-dev-kit/ — never per project — and each tool falls back to your normal flow when it is missing, rate-limited, or out of quota.
| Tool | What the agent gets | Runs | Cost | Verified |
|---|---|---|---|---|
stitch | UI prototyping with Google Stitch (screens, variants, design systems) before any code is written | hosted | free for now, limited | live: generated a mobile screen and a Three.js 3D widget |
local-vision | describe_image — reads screenshots, diagrams and photos for models that cannot see (Qwen3-VL-2B via llama.cpp) | local CPU/GPU | free | real OpenCode; ~7–12 s per image on an 8-core CPU |
image-gen | generate_image for websites and apps: Gemini "Nano Banana" → Cloudflare Workers AI FLUX.1 schnell → labelled placeholder | hosted | Cloudflare free tier ≈ 173 images/day; Gemini image models need billing | real OpenCode; live Cloudflare and Gemini quota paths |
prefilter | prefilter — the local Laya decision model ranks candidate files/tests before the agent reads them in full | local | free | real OpenCode; 1–3 s per call after a ~15–20 s first start |
Built so a free tool can never become a trap. image-gen warns at 80% of a daily cap, switches provider at 90% (before anything fails or bills), pauses all generation until 00:00 UTC when every provider is exhausted, then serves clearly marked placeholders — and every switch, pause and placeholder is reported to you as a notice, in the tool output and in ~/.ai-dev-kit/image-gen/notices.log. Installers run a hardware preflight (CPU, RAM, disk, GPU) first, refuse installs the machine cannot hold, and pick CUDA/Vulkan/Metal builds only when the hardware warrants it, with automatic CPU fallback.
Honest status. Developed and tested on Windows 11 with an 8-core CPU and no discrete GPU. The CUDA, Metal, discrete-GPU Vulkan and Linux/macOS paths are implemented but untested. Model and API facts carry a verified: date and are re-checked live at setup. Credits and licences for every model and service (Qwen, llama.cpp, Laya, FLUX.1, Gemini, Stitch, placehold.co) are in CREDITS.md and each server file; nothing third-party is bundled in this repo.
Get started: install the kit, then tell your agent "set up MCP servers" — mcp-manager asks which tools, which AI clients, and where to keep keys, then writes the configs (merging, never overwriting) and health-checks each one.
skills/ — and coding-standards/ if the project should carry the standards — into the project root.read skills/README.md and install
.claude/skills/ for Claude Code, .opencode/skills/ for OpenCode), wires the standards into that tool's session file (CLAUDE.md or AGENTS.md respectively), and reports what was installed.Details, including per-skill manual installs: skills/README.md → Installing into a new project.
learnings.md sidecar at start and appends one distilled lesson at end. Learnings are per-project (never committed here, never copied by the installer) — each project's copies adapt to that project.role-session skill coordinates multiple parallel AI sessions — Claude Code, OpenCode, or both (role charters, atomic mkdir file claims, a git commit token, session ids) and switches itself off in projects without the parallel structure.memory-gardener skill prunes accumulated knowledge.intent-capture pins down goal/constraints/done-when on ambiguous asks before any plan is made; pre-merge-gate re-checks a diff against the loaded standards before a commit or review handoff — both are prose protocols, not tool-specific.hooks-enforcement) can mechanically assist a rule, it lives under skills/models/claude/ as an opt-in add-on — the underlying contract in coding-standards/ai-standards.md works the same with or without it, on any tool.code-audit checks 46 script-checkable rules (CSS, HTML, JS/TS, page SEO/accessibility/performance basics, git history, versioning) across a whole project, groups violations by rule with the worst files first, lists file:line on request and keeps a baseline so counts can only go down. It also lists what it cannot check, so the agent knows what still needs reading.mods/ ships six opt-in mods installed through mod-setup: budget-ledger, precommit-gate, guard, edit-check, debug-nudge and standards-chain. Each wraps a tool-agnostic script or a few pure rules, fails open, and nothing in the kit depends on them.plan-first can hand a written plan to the next cheaper model through delegate.js, with commit and push blocked for the executor, then verify and fix the result itself.haiku-5-5 covers the new adaptive-thinking Haiku, its breaking API changes against 4.5 and how to route to it.package.json at the skill root so the CommonJS companion scripts keep running inside Astro, Vite and other "type": "module" projects.mcp-manager is a meta skill that owns setup interviews, per-client config writing (Claude Code, OpenCode, others), MCP-first-then-fallback routing and global-first layout for every optional MCP server; adding a server is one data file. Shipped servers: Stitch UI prototyping, local image reading (Qwen3-VL), image generation with provider fallback and limit protection, and the Laya-backed file prefilter. All run from one global home (~/.ai-dev-kit/) and were verified in a real OpenCode session.system1-prefilter now supports a free offline provider (Laya, 421M parameters) with a hardware-aware installer and a prefilter MCP tool; measured guidance on phrasing, checkpoint choice and thresholds is recorded in the skill.skill-writer now asks for imperative steps, one term per concept and explicit conditions (borrowed from ASD-STE100's disambiguation rules). Measured on two skills: about 16% fewer tokens with equal-or-better rule-following.install-kit now detects the target tool and routes both the skill root and the session-protocol file (CLAUDE.md for Claude Code, AGENTS.md for OpenCode/Codex) — a contract in a file the tool never opens can no longer pass as a successful install. Parallel-session claims moved from a shared locks.md table to atomic handover/locks.d/ claims with per-session ids, so concurrent Claude Code and OpenCode sessions can't silently clobber each other.agent-usage defaults every session to inline work — no Agent-tool delegation — unless the user names an agent explicitly or a scope-anchored need is judged and approved first; mode-kernel is the shared table governing how that gate (and three other skills' own stop-and-wait gates) behaves across autonomous, planning, and background session modes.skill-ablation is a periodic companion to memory-gardener: archive the accumulated session-protocol file/skills/hooks, run real work with none of it, and restore only what repeated real-world evidence proves is still needed — catching obsolete instructions a line-count cap alone can't.shopify-toolkit-install clones Shopify's own AI toolkit straight into a target project's skill root at install time — this kit never stores or forks Shopify-authored files, so authorship and their telemetry hook stay exactly where they belong.compat: <tool>-only frontmatter field, checked automatically by install-kit at install time — so tool-specific features get flagged and kept out of incompatible projects instead of failing silently.comment-style sets one project-wide level for AI-written code comments — none (default), terse, or descriptive — because comments cost output tokens when written and input tokens on every re-read. install-kit asks at install; the user can change it any time in-session. Rule-mandated comments (TS as justifications, suppression-directive reasons, tool pragmas) still apply at none.sonnet-5-5, opus-5-5, opus-5, and fable-5-1 join the lineup, and claude-all-models now routes across the current gears with the previous generation kept for pinned projects. These stay universal (no compat field) — they encode model behavior and API rules, not any one tool's mechanisms — and each new-generation skill flags the breaking API changes (forced tool_choice rejected, thinking blocks bound to model + conversation) that bite when moving up from its predecessor.system1-prefilter introduces a fourth skill-scope category — a capability that's never stack-detected or installed by default, only turned on when the user explicitly asks, then stays pinned as their own durable choice. skill-scope now tracks this category alongside permanent-pinned and per-session-archivable.skills/
README.md ← catalog + install instructions (start here)
models/ ← per-model protocols + fleet routing (claude/ lineup + Claude Code hooks, opencode/ open-weight)
workflow/ ← intent capture, planning, handover, debugging, budget, commits, pre-merge checks, perf, agent-usage discipline, session-mode gating, parallel sessions, e2e scaffolding
standards/ ← the coding-standards enforcement skill
memory/ ← repo-committed memory, knowledge gardening, periodic skill ablation
stack/ ← Three.js, Astro, Shopify (live-pulled from Shopify's own toolkit, never forked)
libraries/ ← skills for the author's own libraries (strata-css, triforge)
meta/ ← skill-writer (quality bar), install-kit (installer), skill-scope (pin/archive/addon classification)
addons/ ← opt-in, user-opted capabilities, never installed by default (system1-prefilter + local Laya)
mcp/ ← opt-in MCP servers: mcp-manager + one companion per server (stitch, local-vision, image-gen)
mods/ ← optional Claude Code mods (budget-ledger, precommit-gate, guard, edit-check, debug-nudge, standards-chain), a local marketplace; each wraps a tool-agnostic script from a skill
tests/ ← tests for the kit's scripts (node tests/<name>.test.js) plus real captured logs under fixtures/
migrations/
RENAMES.md ← skill rename ledger — install-kit reads it to migrate old installs (never delete)
coding-standards/
index.md ← system map: layers, reading order, file-to-role mapping (start here)
*-standards.md ← global rules per discipline (css, html, js/ts, git, seo, a11y, qa, ai, …)
*/ ← file-role partials per discipline
frameworks/ ← framework additions/overrides (astro, bootstrap)
tooling/ ← lint configs enforcing the machine-checkable rules
CLAUDE.example.md← session-protocol template — wire into CLAUDE.md (Claude Code) or AGENTS.md (OpenCode)
coding-standards/tooling/ in the same commit.learnings.md files are gitignored — they belong to the project that earned them. Lessons worth keeping forever get promoted into skill bodies (see memory-gardener).What is an agent skill (SKILL.md)? A markdown file with a short description of when it applies and the steps to follow. The agent reads the description, and loads the file only when a task matches. Every skill here is one such file.
Which AI coding tools does it work with? Claude Code natively, plus OpenCode, Codex and any agent that reads markdown instructions. The installer detects the tool and puts the skills and the session file where that tool actually reads them (CLAUDE.md or AGENTS.md).
Does it install packages or touch my code? No. The installer copies markdown files only. The audit script and the Claude Code mods are optional and run only when you choose them.
Can I use only the standards, or only the skills? Yes, either folder works alone. A project that wants the standards enforced during a session needs both, because the coding-standards skill is what loads the chain.
How is this different from a prompt library? Skills load on a trigger instead of sitting in every prompt, they record lessons per project, the standards are testable rules with wrong/right examples, and a script audits a codebase against them.
**How do I update an install?
hooks/register.ts 44 lines1import type { Register } from 'claude-code'
2import { ASK, checkBash, checkPath, type Config } from './rules'
3
4// Deliberate denies for protected files and destructive commands. If the guard itself breaks, the
5// call goes through (fails open), so a bug here cannot stop all work. Never touches handover/.
6let cfg: Config | null = null
7
8async function config($: any): Promise<Config> {
9 if (cfg) return cfg
10 cfg = {}
11 try {
12 if (await $.fs.exists('.claude/guard.json')) cfg = JSON.parse(await $.fs.read('.claude/guard.json')) as Config
13 } catch {
14 cfg = {} // a broken config file leaves only the built-in rules
15 }
16 return cfg
17}
18
19export const register: Register = (on) => {
20 on('session.start', async ($, e, next) => {
21 cfg = null // re-read the config on each session start and reload
22 return next(e)
23 }).catch(($, e, next) => next(e))
24
25 on('tool.call', { tool: ['Edit', 'Write', 'NotebookEdit'] }, async ($, e, next) => {
26 const a = e as unknown as Record<string, unknown>
27 const path = String(a.file_path ?? a.notebook_path ?? '')
28 const why = path ? checkPath(path, await config($)) : null
29 return why ? { deny: `guard: editing ${path} is blocked (${why}).${ASK}` } : next(e)
30 }).catch(($, e, next) => next(e))
31
32 // PowerShell too: on Windows the model may run git through it, and a Bash-only guard would be skipped.
33 on('tool.call', { tool: ['Bash', 'PowerShell'] }, async ($, e, next) => {
34 const c = await config($)
35 let branch: string | undefined
36 if ((c.protectedBranches?.length ?? 0) > 0 && /\bpush\b/.test(e.command)) {
37 const r = await $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { timeoutMs: 5000 })
38 if (r.exitCode === 0) branch = r.stdout.trim()
39 }
40 const why = checkBash(e.command, c, branch, e.tool === 'PowerShell')
41 return why ? { deny: `guard: blocked (${why}).${ASK}` } : next(e)
42 }).catch(($, e, next) => next(e))
43}
44hooks/rules.ts 139 lines1// Pure rules for the guard mod: no host calls, so they are unit-tested directly.
2// Never add a rule for paths under handover/: lane coordination there belongs to the model-driven
3// claim protocol (hooks-enforcement), and a deny would block it.
4export type Config = {
5 protectedPaths?: string[] // globs, relative to the project root
6 allowPaths?: string[]
7 protectedBranches?: string[] // pushes to these are denied
8 allowCommands?: string[] // regex strings; a match skips every Bash rule
9 denyCommands?: string[] // regex strings
10}
11
12export const ASK = ' Ask the user first. They can run it themselves with the ! prefix, or allow it in .claude/guard.json.'
13const SEP = String.fromCharCode(92)
14export const norm = (p: string): string => p.split(SEP).join('/')
15
16export function globToRegExp(glob: string): RegExp {
17 const esc = norm(glob).replace(/[.+^${}()|[\]]/g, (c) => SEP + c)
18 const g = esc
19 .replace(/\*\*\//g, '\u0001')
20 .replace(/\*\*/g, '\u0002')
21 .replace(/\*/g, '[^/]*')
22 .replace(/\?/g, '[^/]')
23 .replace(/\u0001/g, '(?:.*/)?')
24 .replace(/\u0002/g, '.*')
25 return new RegExp('(^|/)' + g + '$')
26}
27
28const PATH_RULES: [RegExp, string][] = [
29 [/(^|\/)\.env(\.(?!example$|sample$|template$)[^/]+)?$/i, 'environment file that holds secrets'],
30 [/(^|\/)(id_rsa|id_ed25519)(\.pub)?$|\.(pem|pfx|p12|key)$/i, 'private key or certificate'],
31 [/(^|\/)\.git\//, '.git internals'],
32 [/(^|\/)node_modules\//, 'installed dependencies'],
33 [/(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|Cargo\.lock|Gemfile\.lock|poetry\.lock)$/, 'lockfile: let the package manager change it'],
34]
35
36export function checkPath(path: string, cfg: Config = {}): string | null {
37 const p = norm(path)
38 if ((cfg.allowPaths ?? []).some((g) => globToRegExp(g).test(p))) return null
39 for (const [re, why] of PATH_RULES) if (re.test(p)) return why
40 for (const g of cfg.protectedPaths ?? []) if (globToRegExp(g).test(p)) return 'protected in .claude/guard.json'
41 return null
42}
43
44// ---- Bash and PowerShell ----
45// ps = PowerShell: the escape character is the backtick (a backslash is a path separator), '' is a quote
46// inside single quotes, and a lone & is the call operator (& git push), not a separator.
47export function segments(cmd: string, ps = false): string[][] {
48 const ESC = ps ? '`' : SEP
49 const out: string[][] = []
50 let toks: string[] = []
51 let cur = ''
52 let has = false
53 let q: string | null = null
54 const endTok = () => { if (has) toks.push(cur); cur = ''; has = false }
55 const endSeg = () => { endTok(); if (toks.length) out.push(toks); toks = [] }
56 for (let i = 0; i < cmd.length; i++) {
57 const c = cmd[i]
58 if (q) {
59 if (c === q) { if (ps && q === "'" && cmd[i + 1] === "'") { cur += "'"; i++ } else q = null }
60 else if (c === ESC && q === '"' && i + 1 < cmd.length) cur += cmd[++i]
61 else cur += c
62 } else if (c === '"' || c === "'") { q = c; has = true }
63 else if (c === ESC && i + 1 < cmd.length) { cur += cmd[++i]; has = true }
64 else if (/\s/.test(c) && c !== '\n') endTok()
65 else if (c === '\n' || c === ';') endSeg()
66 else if (c === '&' || c === '|') {
67 if (ps && c === '&' && cmd[i + 1] !== '&') { endTok(); continue }
68 endSeg(); if (cmd[i + 1] === c) i++
69 }
70 else { cur += c; has = true }
71 }
72 endSeg()
73 return out
74}
75
76const baseName = (t: string) => norm(t).split('/').pop()!.replace(/\.exe$/i, '').toLowerCase()
77const PS_REMOVE = ['remove-item', 'ri', 'rm', 'rmdir', 'rd', 'del', 'erase'] // Remove-Item and its aliases
78
79function dangerousTarget(t: string): boolean {
80 const p = norm(t).replace(/\/\*$/, '/').replace(/^(\$env:USERPROFILE|\$\{env:USERPROFILE\}|\$env:HOME)(?=\/|$)/i, '~')
81 if (['/', '~', '~/', '*', '.', '..', './', '../', '$HOME', '$HOME/', '${HOME}', '${HOME}/', '.*'].includes(p)) return true
82 if (/^[A-Za-z]:\/?$/.test(p)) return true // C:\ and C:\*
83 if (/^\/[^/]+\/?$/.test(p)) return true // /usr, /etc
84 if (/^\.\.(\/|$)/.test(p)) return true // anything in the parent directory
85 if (/^(~|\$HOME|\$\{HOME\})\/[^/]+\/?$/.test(p)) return true // ~/Documents
86 return false
87}
88
89function gitCheck(rest: string[], cfg: Config, currentBranch?: string): string | null {
90 let i = 0
91 while (i < rest.length && rest[i].startsWith('-')) i += rest[i] === '-c' || rest[i] === '-C' ? 2 : 1
92 const sub = rest[i]
93 const args = rest.slice(i + 1)
94 const has = (...f: string[]) => args.some((a) => f.includes(a))
95 if (sub === 'push') {
96 // -f also inside a cluster (-fu); --mirror overwrites and deletes remote refs; +ref is a per-branch force
97 if (has('--force', '--force-if-includes', '--mirror') || args.some((a) => /^-[a-zA-Z]*f[a-zA-Z]*$/.test(a) || /^\+[^+]/.test(a))) return 'force push rewrites remote history'
98 const positional = args.filter((a) => !a.startsWith('-'))
99 const deleting = has('--delete') || args.some((a) => /^-[a-zA-Z]*d[a-zA-Z]*$/.test(a))
100 const deleted = deleting ? positional.slice(1) : positional.slice(1).filter((r) => /^:[^:]/.test(r)).map((r) => r.slice(1))
101 const keep = ['main', 'master', ...(cfg.protectedBranches ?? [])]
102 const gone = deleted.map((r) => r.split(':').pop()!).find((b) => keep.includes(b))
103 if (gone) return `deleting the remote branch ${gone}`
104 const protectedB = cfg.protectedBranches ?? []
105 if (protectedB.length) {
106 const targets = positional.slice(1).map((a) => a.split(':').pop()!) // skip the remote name
107 const hit = targets.find((t) => protectedB.includes(t)) ?? (targets.length === 0 && currentBranch && protectedB.includes(currentBranch) ? currentBranch : undefined)
108 if (hit) return `direct push to protected branch ${hit}`
109 }
110 }
111 if (sub === 'reset' && has('--hard')) return 'git reset --hard discards uncommitted work'
112 if (sub === 'clean' && !has('-n', '--dry-run') && args.some((a) => a === '--force' || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(a))) return 'git clean -f deletes untracked files for good'
113 if (sub === 'checkout' && (has('-f', '--force') || (args.includes('.') && !args.includes('-b')))) return 'git checkout discards working-tree changes'
114 if (sub === 'restore' && args.includes('.') && !(has('--staged') && !has('--worktree', '-W'))) return 'git restore . discards working-tree changes'
115 return null
116}
117
118// ps: the command came through the PowerShell tool, not Bash.
119export function checkBash(cmd: string, cfg: Config = {}, currentBranch?: string, ps = false): string | null {
120 if ((cfg.allowCommands ?? []).some((r) => new RegExp(r).test(cmd))) return null
121 for (const r of cfg.denyCommands ?? []) if (new RegExp(r).test(cmd)) return 'blocked in .claude/guard.json'
122 if (/\b(curl|wget)\b[^|;&\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b/.test(cmd)) return 'piping a download into a shell'
123 if (/\b(iwr|irm|invoke-webrequest|invoke-restmethod|curl|wget)\b[^;\n]*\|\s*(iex|invoke-expression)\b/i.test(cmd)) return 'piping a download into a shell'
124 if (/\b(mysql|mariadb|psql|sqlite3|mongosh?)\b/.test(cmd) && /\bdrop\s+(table|database|schema)\b/i.test(cmd)) return 'dropping database objects'
125 for (let toks of segments(cmd, ps)) {
126 while (toks.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[0]) || baseName(toks[0]) === 'sudo')) toks = toks.slice(1)
127 if (!toks.length) continue
128 const bin = baseName(toks[0])
129 const rest = toks.slice(1)
130 const remover = bin === 'rm' || (ps && PS_REMOVE.includes(bin))
131 if (remover && rest.some((t) => t === '--recursive' || /^-[a-zA-Z]*[rR][a-zA-Z]*$/.test(t)) && rest.some((t) => !t.startsWith('-') && dangerousTarget(t))) return 'recursive delete of a root-like path'
132 if (bin === 'git') { const why = gitCheck(rest, cfg, currentBranch); if (why) return why }
133 if (['npm', 'yarn', 'pnpm', 'bun'].includes(bin) && rest[0] === 'publish') return 'publishing a package is irreversible'
134 if (bin === 'cargo' && rest[0] === 'publish') return 'publishing a crate is irreversible'
135 if (bin === 'twine' && rest[0] === 'upload') return 'publishing a package is irreversible'
136 }
137 return null
138}
139