ai-guard-plugins 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.yaml ADDED
@@ -0,0 +1,49 @@
1
+ name: AI Guard Plugins Installer
2
+ description: Install production-hardened plugins for Claude Code & OpenCode
3
+
4
+ steps:
5
+ - id: platform
6
+ type: select
7
+ message: Which AI coding assistant are you using?
8
+ choices:
9
+ - label: OpenCode
10
+ value: opencode
11
+ - label: Claude Code
12
+ value: claude
13
+ - label: Cline
14
+ value: cline
15
+ - label: Cursor
16
+ value: cursor
17
+ - label: Kilo Code
18
+ value: kilo
19
+
20
+ - id: plugins
21
+ type: multiselect
22
+ message: Which plugins do you want to install?
23
+ choices:
24
+ - label: "๐Ÿ’ณ billing-guard โ€” Catch billing errors before infinite retry freezes"
25
+ value: billing-guard
26
+ checked: true
27
+ - label: "๐Ÿงน auto-lint-fix โ€” Auto-format code after every write/edit"
28
+ value: auto-lint-fix
29
+ checked: true
30
+ - label: "๐Ÿ“ฆ auto-summarize-input โ€” Compress large inputs to save tokens"
31
+ value: auto-summarize-input
32
+ checked: true
33
+ - label: "๐Ÿง  capture-insights โ€” Learn from recurring mistakes"
34
+ value: capture-insights
35
+ checked: true
36
+ - label: "๐Ÿ“ cursorrules-enforcer โ€” Enforce coding standards on writes"
37
+ value: cursorrules-enforcer
38
+ checked: true
39
+ - label: "๐Ÿ” dry-guard โ€” Warn before duplicating existing code"
40
+ value: dry-guard
41
+ checked: true
42
+
43
+ - id: confirm
44
+ type: confirm
45
+ message: "Install selected plugins?"
46
+
47
+ output:
48
+ platform: "{{ platform }}"
49
+ plugins: "{{ plugins }}"
package/llms.txt ADDED
@@ -0,0 +1,33 @@
1
+ # ai-guard-plugins
2
+
3
+ Production-hardened plugins for Claude Code & OpenCode AI coding assistants.
4
+
5
+ ## What this repo contains
6
+
7
+ 6 TypeScript plugins that intercept AI agent behavior to prevent costly mistakes:
8
+
9
+ 1. billing-guard.ts โ€” Detects billing/quota errors and interrupts before infinite retry freezes
10
+ 2. auto-lint-fix.ts โ€” Auto-runs Biome formatter after every file write/edit
11
+ 3. auto-summarize-input.ts โ€” Compresses inputs >10K chars via Claude Haiku to save context tokens
12
+ 4. capture-insights.ts โ€” Tracks recurring mistake patterns across sessions in persistent JSONL
13
+ 5. cursorrules-enforcer.ts โ€” Blocks writes containing `as any`, `@ts-ignore`, TODOs, require()
14
+ 6. dry-guard.ts โ€” Warns before creating files that duplicate existing code in the project
15
+
16
+ ## Plugin API
17
+
18
+ All plugins implement the `Plugin` type from `@opencode-ai/plugin` and use these hooks:
19
+ - `tool.execute.before` โ€” intercept before a tool runs (can throw to block)
20
+ - `tool.execute.after` โ€” react after a tool completes
21
+ - `event` โ€” listen to session events like `message.updated` and `session.idle`
22
+ - `tui.prompt.append` โ€” intercept user input before it reaches the model
23
+
24
+ ## Installation
25
+
26
+ Plugins are standalone .ts files copied to `~/.config/opencode/plugins/` (OpenCode) or `~/.claude/plugins/` (Claude Code).
27
+
28
+ ## Key patterns
29
+
30
+ - Plugins throw Error to block operations (cursorrules-enforcer, billing-guard)
31
+ - Plugins use console.warn for advisory warnings (dry-guard)
32
+ - Plugins use persistent storage via JSONL files (capture-insights)
33
+ - Plugins shell out via `$` tagged template for system commands (auto-lint-fix, dry-guard)
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "ai-guard-plugins",
3
+ "version": "1.1.0",
4
+ "description": "> Production-hardened plugins for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) & [OpenCode](https://opencode.ai) โ€” battle-tested hooks from real AI-assisted development that catch costly mistakes before they happen.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/YosefHayim/ai-guard-plugins.git"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "type": "commonjs",
17
+ "bugs": {
18
+ "url": "https://github.com/YosefHayim/ai-guard-plugins/issues"
19
+ },
20
+ "homepage": "https://github.com/YosefHayim/ai-guard-plugins#readme",
21
+ "devDependencies": {
22
+ "pr-prism": "^1.1.6"
23
+ }
24
+ }
@@ -0,0 +1,39 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const LINTABLE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cts', '.cjs']);
4
+ const SKIP_PATHS = ['node_modules', 'dist', '.next', 'build', '.turbo'];
5
+ const BIOME = process.env.BIOME_PATH ?? 'biome';
6
+
7
+ function getExt(filePath: string): string {
8
+ const dot = filePath.lastIndexOf('.');
9
+ return dot === -1 ? '' : filePath.slice(dot);
10
+ }
11
+
12
+ export const AutoLintFixPlugin: Plugin = async ({ $ }) => {
13
+ try {
14
+ await $`which ${BIOME}`.quiet();
15
+ } catch {
16
+ console.warn('[auto-lint-fix] biome not found โ€” plugin disabled');
17
+ return {};
18
+ }
19
+
20
+ return {
21
+ 'tool.execute.after': async (input, _output) => {
22
+ if (input.tool !== 'write' && input.tool !== 'edit') return;
23
+
24
+ const args = (_output?.args ?? input.args) as Record<string, unknown> | undefined;
25
+ if (!args) return;
26
+
27
+ const filePath = (args.filePath ?? args.file_path) as string | undefined;
28
+ if (!filePath) return;
29
+
30
+ if (!LINTABLE_EXTS.has(getExt(filePath))) return;
31
+ const normalizedPath = filePath.replace(/\\/g, '/');
32
+ if (SKIP_PATHS.some((p) => normalizedPath.includes(`/${p}/`) || normalizedPath.startsWith(`${p}/`))) return;
33
+
34
+ try {
35
+ await $`${BIOME} check --fix --unsafe ${filePath}`.quiet().nothrow();
36
+ } catch {}
37
+ },
38
+ };
39
+ };
@@ -0,0 +1,45 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const THRESHOLD = 10000;
4
+
5
+ export const AutoSummarizeInputPlugin: Plugin = async ({ client }) => {
6
+ return {
7
+ 'tui.prompt.append': async (_input, output) => {
8
+ const text = String(output?.text ?? '');
9
+ if (text.length <= THRESHOLD) return;
10
+
11
+ try {
12
+ const charCount = text.length;
13
+ console.log(`๐Ÿ“ฆ Input too large (${charCount} chars). Auto-summarizing via Claude...`);
14
+
15
+ const result = await client.chat.create({
16
+ body: {
17
+ model: 'claude-haiku-4-5-20250415',
18
+ max_tokens: 2000,
19
+ messages: [
20
+ {
21
+ role: 'user',
22
+ content: `Summarize this input for a coding assistant. Keep ALL code snippets, file paths, error messages, and requirements verbatim. Remove ONLY redundancy, repetition, and filler. Output ONLY the summarized input:\n\n${text}`,
23
+ },
24
+ ],
25
+ },
26
+ });
27
+
28
+ const summary =
29
+ result?.content?.find?.((b: Record<string, unknown>) => b.type === 'text')?.text ??
30
+ result?.content?.[0]?.text ??
31
+ '';
32
+
33
+ if (summary && summary.length < charCount && output?.text === text) {
34
+ const reduction = Math.round(((charCount - summary.length) / charCount) * 100);
35
+ output.text = `[Auto-summarized: ${charCount} chars โ†’ ${summary.length} chars (${reduction}% reduction)]\n\n${summary}`;
36
+ console.log(
37
+ `๐Ÿ“ฆ Auto-summarized: ${charCount} โ†’ ${summary.length} chars (${reduction}% saved)`,
38
+ );
39
+ }
40
+ } catch (err) {
41
+ console.warn(`[auto-summarize] Failed: ${err}`);
42
+ }
43
+ },
44
+ };
45
+ };
@@ -0,0 +1,101 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const BILLING_PATTERNS = [
4
+ /No payment method/i,
5
+ /Add a payment method/i,
6
+ /account is not active.*billing/i,
7
+ /billing details/i,
8
+ /payment.*required/i,
9
+ /quota.*exceeded/i,
10
+ /insufficient.*funds/i,
11
+ /rate limit.*billing/i,
12
+ /plan.*expired/i,
13
+ /subscription.*inactive/i,
14
+ ];
15
+
16
+ function isBillingError(text: string): string | null {
17
+ for (const pattern of BILLING_PATTERNS) {
18
+ const match = text.match(pattern);
19
+ if (match) return match[0];
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function safeStringify(value: unknown): string {
25
+ try {
26
+ return typeof value === 'string' ? value : JSON.stringify(value);
27
+ } catch {
28
+ return String(value);
29
+ }
30
+ }
31
+
32
+ function extractText(msg: Record<string, unknown>): string {
33
+ if (typeof msg.content === 'string') return msg.content;
34
+ if (Array.isArray(msg.content)) {
35
+ return (msg.content as Array<Record<string, unknown>>)
36
+ .map((p: Record<string, unknown>) => (typeof p.text === 'string' ? p.text : ''))
37
+ .join(' ');
38
+ }
39
+ if (typeof msg.text === 'string') return msg.text;
40
+ if (typeof msg.output === 'string') return msg.output;
41
+ if (typeof msg.error === 'string') return msg.error;
42
+ if (typeof msg.result === 'string') return msg.result;
43
+ if (Array.isArray(msg.parts)) {
44
+ return (msg.parts as Array<Record<string, unknown>>)
45
+ .map((p: Record<string, unknown>) => (typeof p.text === 'string' ? p.text : ''))
46
+ .join(' ');
47
+ }
48
+ return '';
49
+ }
50
+
51
+ export const BillingGuardPlugin: Plugin = async () => {
52
+ return {
53
+ 'tool.execute.after': async (_input, output) => {
54
+ const result = output?.result;
55
+ if (!result) return;
56
+
57
+ const text = safeStringify(result);
58
+ const match = isBillingError(text);
59
+
60
+ if (match) {
61
+ console.error(`\n๐Ÿ’ณ BILLING ERROR DETECTED: "${match}"`);
62
+ console.error('โšก Interrupting to prevent infinite retry freeze.\n');
63
+ throw new Error(
64
+ `BILLING ERROR: ${match}. The API provider requires payment. Fix billing before retrying. Do NOT retry this operation.`,
65
+ );
66
+ }
67
+ },
68
+
69
+ 'tool.execute.before': async (input, output) => {
70
+ if (input.tool !== 'task') return;
71
+
72
+ const args = (output?.args ?? input.args) as Record<string, unknown> | undefined;
73
+ if (!args) return;
74
+
75
+ const prompt = String(args.prompt ?? '');
76
+ const match = isBillingError(prompt);
77
+ if (match) {
78
+ throw new Error(
79
+ `BILLING ERROR in task prompt: ${match}. Do NOT delegate โ€” billing issue must be resolved first.`,
80
+ );
81
+ }
82
+ },
83
+
84
+ event: async ({ event }) => {
85
+ if (event.type !== 'message.updated') return;
86
+
87
+ const msg = event.properties as Record<string, unknown>;
88
+ const text = extractText(msg);
89
+ if (!text) return;
90
+
91
+ const match = isBillingError(text);
92
+ if (match) {
93
+ console.error(`\n๐Ÿ’ณ BILLING ERROR in message stream: "${match}"`);
94
+ console.error('โšก Agent must stop all operations and notify the user.\n');
95
+ throw new Error(
96
+ `BILLING ERROR: ${match}. STOP all operations. Do NOT retry or delegate. Notify the user that billing needs to be fixed.`,
97
+ );
98
+ }
99
+ },
100
+ };
101
+ };
@@ -0,0 +1,215 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+
3
+ const MEMORY_FILE = `${process.env.HOME ?? require('node:os').homedir()}/.claude/memory/insights.jsonl`;
4
+
5
+ interface InsightEntry {
6
+ id: string;
7
+ insight: string;
8
+ count: number;
9
+ last: string;
10
+ projects: string;
11
+ }
12
+
13
+ const PATTERN_CHECKS: Array<{ regex: RegExp; id: string; insight: string }> = [
14
+ {
15
+ regex: /DRY|don't repeat|already exists|duplicat/i,
16
+ id: 'dry-violation',
17
+ insight:
18
+ 'Check existing files before creating new ones. Extract shared utils when there is clear reuse.',
19
+ },
20
+ {
21
+ regex: /unused import|unused variable|dead code/i,
22
+ id: 'unused-code',
23
+ insight: 'Biome --fix handles unused imports/variables automatically.',
24
+ },
25
+ {
26
+ regex: /as any|ts-ignore|ts-expect-error/i,
27
+ id: 'type-safety-bypass',
28
+ insight:
29
+ 'Never use as any, @ts-ignore, or @ts-expect-error. Use the most specific correct type.',
30
+ },
31
+ {
32
+ regex: /refactor.*minimal|minimal change|scope creep/i,
33
+ id: 'scope-discipline',
34
+ insight: 'Make the minimal change needed. Do not refactor while fixing.',
35
+ },
36
+ {
37
+ regex: /circular dependency|circular import/i,
38
+ id: 'circular-dep',
39
+ insight: 'Restructure module boundaries to break circular dependencies.',
40
+ },
41
+ {
42
+ regex: /re-export|barrel file|index file.*logic/i,
43
+ id: 're-export',
44
+ insight: 'Avoid re-exports. Index files may only re-export modules, never logic.',
45
+ },
46
+ {
47
+ regex: /process\.cwd|project root.*monorepo/i,
48
+ id: 'path-resolution',
49
+ insight:
50
+ 'Never assume process.cwd() equals project root in monorepos. Use explicit root resolver.',
51
+ },
52
+ {
53
+ regex: /migration|schema change|database migrat/i,
54
+ id: 'data-migration',
55
+ insight: 'Verify schema + data integrity after migrations. Make migrations idempotent.',
56
+ },
57
+ {
58
+ regex: /workaround|hack|bandaid|band-aid/i,
59
+ id: 'workaround',
60
+ insight: 'Document why workaround was needed and plan a proper fix.',
61
+ },
62
+ {
63
+ regex: /performance|slow query|optimize|bottleneck/i,
64
+ id: 'performance',
65
+ insight: "Profile before optimizing. Measure, don't guess.",
66
+ },
67
+ {
68
+ regex: /security|secret|credential|api.key|token leak/i,
69
+ id: 'security',
70
+ insight: 'Review credentials handling. Never commit secrets.',
71
+ },
72
+ {
73
+ regex: /test.*mock.*implementation|mock.*detail/i,
74
+ id: 'test-mock',
75
+ insight: 'Mock behavior (modules), not implementation details (process.cwd, process.env).',
76
+ },
77
+ ];
78
+
79
+ function readEntries(): Map<string, InsightEntry> {
80
+ const map = new Map<string, InsightEntry>();
81
+ try {
82
+ const fs = require('node:fs');
83
+ if (!fs.existsSync(MEMORY_FILE)) return map;
84
+ const lines = fs.readFileSync(MEMORY_FILE, 'utf-8').split('\n').filter(Boolean);
85
+ for (const line of lines) {
86
+ try {
87
+ const entry = JSON.parse(line) as InsightEntry;
88
+ map.set(entry.id, entry);
89
+ } catch {}
90
+ }
91
+ } catch {}
92
+ return map;
93
+ }
94
+
95
+ function writeEntries(entries: Map<string, InsightEntry>): void {
96
+ try {
97
+ const fs = require('node:fs');
98
+ const path = require('node:path');
99
+ const dir = path.dirname(MEMORY_FILE);
100
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
101
+ const content = `${[...entries.values()].map((e) => JSON.stringify(e)).join('\n')}\n`;
102
+ fs.writeFileSync(MEMORY_FILE, content);
103
+ } catch {}
104
+ }
105
+
106
+ export const CaptureInsightsPlugin: Plugin = async ({ directory }) => {
107
+ let sessionBuffer: string[] = [];
108
+
109
+ return {
110
+ event: async ({ event }) => {
111
+ if (event.type === 'message.updated') {
112
+ const msg = event.properties as Record<string, unknown>;
113
+ const text = extractText(msg);
114
+ if (text) {
115
+ sessionBuffer.push(text);
116
+ if (sessionBuffer.length > 50) sessionBuffer = sessionBuffer.slice(-50);
117
+ }
118
+ }
119
+
120
+ if (event.type === 'session.idle') {
121
+ if (sessionBuffer.length === 0) return;
122
+
123
+ const combined = sessionBuffer.join('\n');
124
+ const matched = PATTERN_CHECKS.filter((p) => p.regex.test(combined));
125
+
126
+ if (matched.length === 0) {
127
+ sessionBuffer = [];
128
+ return;
129
+ }
130
+
131
+ const entries = readEntries();
132
+ const now = new Date().toISOString().slice(0, 16);
133
+ const projectName = directory.split('/').pop() ?? 'unknown';
134
+
135
+ for (const m of matched) {
136
+ const existing = entries.get(m.id);
137
+ if (existing) {
138
+ existing.count += 1;
139
+ existing.last = now;
140
+ const projects = existing.projects.split(',').filter(Boolean);
141
+ if (!projects.includes(projectName)) {
142
+ existing.projects += `,${projectName}`;
143
+ }
144
+ } else {
145
+ entries.set(m.id, {
146
+ id: m.id,
147
+ insight: m.insight,
148
+ count: 1,
149
+ last: now,
150
+ projects: projectName,
151
+ });
152
+ }
153
+ }
154
+
155
+ writeEntries(entries);
156
+ squeezy(entries);
157
+
158
+ const captured = matched.map((m) => {
159
+ const e = entries.get(m.id);
160
+ return ` ๐Ÿ“ ${m.id} (count: ${e?.count ?? 1})`;
161
+ });
162
+ console.log(
163
+ `๐Ÿง  Insights captured (${projectName}, ${entries.size} total entries):\n${captured.join('\n')}`,
164
+ );
165
+
166
+ sessionBuffer = [];
167
+ }
168
+ },
169
+ };
170
+ };
171
+
172
+ const SQUEEZY_MAX = 15;
173
+
174
+ function squeezy(entries: Map<string, InsightEntry>): void {
175
+ if (entries.size <= SQUEEZY_MAX) return;
176
+
177
+ const sorted = [...entries.values()].sort((a, b) => b.count - a.count);
178
+ const kept = sorted.slice(0, SQUEEZY_MAX);
179
+ const overflow = sorted.slice(SQUEEZY_MAX);
180
+
181
+ for (const extra of overflow) {
182
+ const familyPrefix = extra.id.split('-')[0];
183
+ const target = kept.find((k) => k.id.split('-')[0] === familyPrefix);
184
+ if (target) {
185
+ target.count += extra.count;
186
+ const combinedInsights = `${target.insight} ${extra.insight}`
187
+ .split('. ')
188
+ .filter((v, i, a) => a.indexOf(v) === i)
189
+ .join('. ');
190
+ target.insight = combinedInsights;
191
+ const combinedProjects = `${target.projects},${extra.projects}`
192
+ .split(',')
193
+ .filter((v, i, a) => a.indexOf(v) === i)
194
+ .join(',');
195
+ target.projects = combinedProjects;
196
+ }
197
+ }
198
+
199
+ const squeezed = new Map<string, InsightEntry>();
200
+ for (const entry of kept) {
201
+ squeezed.set(entry.id, entry);
202
+ }
203
+ writeEntries(squeezed);
204
+ }
205
+
206
+ function extractText(msg: Record<string, unknown>): string {
207
+ if (typeof msg.content === 'string') return msg.content;
208
+ if (typeof msg.text === 'string') return msg.text;
209
+ if (Array.isArray(msg.parts)) {
210
+ return (msg.parts as Array<Record<string, unknown>>)
211
+ .map((p) => (typeof p.text === 'string' ? p.text : ''))
212
+ .join(' ');
213
+ }
214
+ return '';
215
+ }
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # AI Guard Plugin: PostToolUse Hook for Cline
4
+ # Combines: billing-guard (after), auto-lint-fix
5
+ #
6
+ # Requires: jq (https://jqlang.github.io/jq/)
7
+ # Optional: biome (https://biomejs.dev/) for auto-formatting
8
+ # Contract: reads JSON from stdin, outputs JSON to stdout
9
+ # =============================================================================
10
+
11
+ set -euo pipefail
12
+
13
+ INPUT=$(cat)
14
+
15
+ TOOL=$(echo "$INPUT" | jq -r '.postToolUse.tool // empty' 2>/dev/null || true)
16
+ PARAMS=$(echo "$INPUT" | jq -c '.postToolUse.parameters // {}' 2>/dev/null || echo '{}')
17
+ RESULT=$(echo "$INPUT" | jq -r '.postToolUse.result // empty' 2>/dev/null || true)
18
+
19
+ # Default: allow
20
+ allow() { echo '{"cancel":false,"contextModification":"","errorMessage":""}'; exit 0; }
21
+ cancel() { echo "{\"cancel\":true,\"contextModification\":\"\",\"errorMessage\":$(echo "$1" | jq -Rs .)}"; exit 0; }
22
+ inform() { echo "{\"cancel\":false,\"contextModification\":$(echo "$1" | jq -Rs .),\"errorMessage\":\"\"}"; exit 0; }
23
+
24
+ [ -z "$TOOL" ] && allow
25
+
26
+ # =============================================================================
27
+ # 1. BILLING GUARD (after) โ€” detect billing errors in tool results
28
+ # =============================================================================
29
+ if [ -n "$RESULT" ]; then
30
+ if echo "$RESULT" | grep -qiE \
31
+ 'payment.method|payment.required|account.is.not.active.*billing|billing.details|quota.exceeded|insufficient.funds|rate.limit.*billing|plan.expired|subscription.inactive'; then
32
+ cancel "BILLING ERROR detected in tool result. The API provider requires payment. Fix billing before retrying. Do NOT retry this operation."
33
+ fi
34
+ fi
35
+
36
+ # =============================================================================
37
+ # 2. AUTO LINT FIX โ€” run biome after file writes
38
+ # =============================================================================
39
+ if [ "$TOOL" = "write_to_file" ] || [ "$TOOL" = "insert_code_block" ] || [ "$TOOL" = "search_and_replace" ]; then
40
+ FILE_PATH=$(echo "$PARAMS" | jq -r '.path // empty' 2>/dev/null || true)
41
+ [ -z "$FILE_PATH" ] && allow
42
+
43
+ # Only lint code files
44
+ EXT="${FILE_PATH##*.}"
45
+ case "$EXT" in
46
+ ts|tsx|js|jsx|mts|mjs|cts|cjs) ;;
47
+ *) allow ;;
48
+ esac
49
+
50
+ # Skip excluded paths (handle both root-level and nested)
51
+ for skip in node_modules dist .next build .turbo; do
52
+ [[ "$FILE_PATH" == "$skip/"* || "$FILE_PATH" == *"/$skip/"* ]] && allow
53
+ done
54
+
55
+ # Find biome binary
56
+ BIOME="${BIOME_PATH:-biome}"
57
+ if ! command -v "$BIOME" &>/dev/null; then
58
+ if [ -f "./node_modules/.bin/biome" ]; then
59
+ BIOME="./node_modules/.bin/biome"
60
+ else
61
+ allow
62
+ fi
63
+ fi
64
+
65
+ # Run biome fix
66
+ if "$BIOME" check --fix --unsafe "$FILE_PATH" &>/dev/null; then
67
+ inform "[auto-lint-fix] Auto-formatted ${FILE_PATH##*/} with Biome."
68
+ else
69
+ allow
70
+ fi
71
+ fi
72
+
73
+ allow