@entet/ai-agent-guard 0.1.1 → 0.2.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/README.md +2 -1
- package/bin/ai-agent-guard.js +167 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ No install, no config, no account.
|
|
|
14
14
|
|
|
15
15
|
| Category | Examples |
|
|
16
16
|
|---|---|
|
|
17
|
+
| **Agent permission settings** | `.claude/`, `.cursor/`, `.vscode/`, `.codex/` settings — every project MCP server approved automatically, unbounded `Bash(*)`/`Read(~/**)` grants, pre-approved `sudo`/`rm`/`curl`, hooks that POST your session to an HTTP endpoint, wildcard hook URLs, disabled hooks, relaxed default modes, inline secrets, and `--dangerously-skip-permissions` |
|
|
17
18
|
| **Secrets** | AWS keys (`AKIA…`), GitHub tokens (`ghp_/gho_/ghs_/ghu_/ghr_/github_pat_`), Stripe live keys (`sk_live_`), OpenAI / Anthropic keys, private key blocks, DB URLs with embedded credentials, hardcoded `api_key = "…"` assignments |
|
|
18
19
|
| **MCP configs** | `mcp.json`, `.mcp.json`, `claude_desktop_config.json` — unpinned `npx`, root/home filesystem access, inline secrets in `env` |
|
|
19
20
|
| **AI instruction files** | `CLAUDE.md`, `AGENTS.md`, `.cursor/rules`, `.cursorrules`, `.windsurfrules`, `gemini.md`, `copilot-instructions.md` — flagged so you can review them for prompt-injection or risky directives |
|
|
@@ -70,7 +71,7 @@ Runs entirely on your machine. **No network calls, no API key, no telemetry.** T
|
|
|
70
71
|
|
|
71
72
|
## Want this in your IDE?
|
|
72
73
|
|
|
73
|
-
This CLI is the free, open-source companion to the **AI Agent Workspace Guard** plugin for JetBrains IDEs,
|
|
74
|
+
This CLI is the free, open-source companion to the **AI Agent Workspace Guard** plugin for JetBrains IDEs. Same checks, run from `Tools → Scan AI Agent Workspace`, with the results in a tool window: grouped by severity, double-click to jump to the offending line, the evidence and the fix in the panel below, a severity filter, and Markdown export. Known false positives can be silenced with an `aiwg:ignore` comment, and suppressed findings stay in the count.
|
|
74
75
|
|
|
75
76
|
➡️ https://plugins.jetbrains.com/plugin/32116-ai-agent-workspace-guard
|
|
76
77
|
|
package/bin/ai-agent-guard.js
CHANGED
|
@@ -220,6 +220,165 @@ function scanMcpConfig(relPath, text, findings, data) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
const AGENT_SETTINGS_DIRS = ['.claude/', '.cursor/', '.vscode/', '.codex/', '.gemini/'];
|
|
224
|
+
|
|
225
|
+
function isAgentSettingsFile(relPath, base) {
|
|
226
|
+
if (!base.toLowerCase().endsWith('.json')) return false;
|
|
227
|
+
if (base.toLowerCase() === 'claude_desktop_config.json') return true;
|
|
228
|
+
const norm = relPath.replace(/\\/g, '/').toLowerCase();
|
|
229
|
+
return AGENT_SETTINGS_DIRS.some((dir) => norm.includes(dir));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** A grant that starts at a wildcard, the home directory, or the filesystem root. */
|
|
233
|
+
const UNBOUNDED_GRANT = /^(Bash|Read|Edit|Write|WebFetch|WebSearch|Task)\(\s*(\*|~|\/|\/\/|\*\*)/i;
|
|
234
|
+
/** Commands that hand over the machine, the network, or the repository history. */
|
|
235
|
+
const DANGEROUS_GRANT =
|
|
236
|
+
/^Bash\(\s*(sudo|rm|curl|wget|chmod|chown|dd|mkfs|shutdown|reboot|git\s+push\s+--force|npm\s+publish|docker)\b/i;
|
|
237
|
+
|
|
238
|
+
function scanAgentSettings(relPath, text, findings, data) {
|
|
239
|
+
if (!data || typeof data !== 'object') return;
|
|
240
|
+
|
|
241
|
+
if (data.enableAllProjectMcpServers === true) {
|
|
242
|
+
findings.push({
|
|
243
|
+
ruleId: 'agent.auto-approve-mcp',
|
|
244
|
+
description: 'Every MCP server in this project is approved automatically — a server added by a teammate or a merged pull request runs without review',
|
|
245
|
+
severity: 'HIGH',
|
|
246
|
+
file: relPath,
|
|
247
|
+
line: lineOf(text, 'enableAllProjectMcpServers'),
|
|
248
|
+
evidence: 'enableAllProjectMcpServers: true',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (data.disableAllHooks === true) {
|
|
253
|
+
findings.push({
|
|
254
|
+
ruleId: 'agent.hooks-disabled',
|
|
255
|
+
description: 'All agent hooks are disabled — local safety checks will not run',
|
|
256
|
+
severity: 'MEDIUM',
|
|
257
|
+
file: relPath,
|
|
258
|
+
line: lineOf(text, 'disableAllHooks'),
|
|
259
|
+
evidence: 'disableAllHooks: true',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const permissions = data.permissions && typeof data.permissions === 'object' ? data.permissions : {};
|
|
264
|
+
|
|
265
|
+
if (typeof permissions.defaultMode === 'string' &&
|
|
266
|
+
/^(auto|acceptEdits|bypassPermissions)$/i.test(permissions.defaultMode)) {
|
|
267
|
+
findings.push({
|
|
268
|
+
ruleId: 'agent.relaxed-default-mode',
|
|
269
|
+
description: `Default permission mode "${permissions.defaultMode}" does not ask before acting`,
|
|
270
|
+
severity: 'MEDIUM',
|
|
271
|
+
file: relPath,
|
|
272
|
+
line: lineOf(text, 'defaultMode'),
|
|
273
|
+
evidence: `defaultMode: ${permissions.defaultMode}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Only the allow list. The same pattern under deny or ask is the safe configuration, and
|
|
278
|
+
// reporting it would teach people to ignore this tool.
|
|
279
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
280
|
+
for (const raw of allow) {
|
|
281
|
+
const grant = String(raw);
|
|
282
|
+
if (UNBOUNDED_GRANT.test(grant)) {
|
|
283
|
+
findings.push({
|
|
284
|
+
ruleId: 'agent.unbounded-permission',
|
|
285
|
+
description: `Agent is granted "${grant}" without bounds — it reaches every file the IDE user can read`,
|
|
286
|
+
severity: 'HIGH',
|
|
287
|
+
file: relPath,
|
|
288
|
+
line: lineOf(text, grant),
|
|
289
|
+
evidence: grant,
|
|
290
|
+
});
|
|
291
|
+
} else if (DANGEROUS_GRANT.test(grant)) {
|
|
292
|
+
findings.push({
|
|
293
|
+
ruleId: 'agent.dangerous-permission',
|
|
294
|
+
description: `Agent is pre-approved to run "${grant}" without a prompt`,
|
|
295
|
+
severity: 'HIGH',
|
|
296
|
+
file: relPath,
|
|
297
|
+
line: lineOf(text, grant),
|
|
298
|
+
evidence: grant,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const hooks = data.hooks && typeof data.hooks === 'object' ? data.hooks : {};
|
|
304
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
305
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
306
|
+
if (entry && typeof entry === 'object' && entry.type === 'http') {
|
|
307
|
+
findings.push({
|
|
308
|
+
ruleId: 'agent.http-hook',
|
|
309
|
+
description: `Hook "${event}" forwards session activity to an HTTP endpoint`,
|
|
310
|
+
severity: 'HIGH',
|
|
311
|
+
file: relPath,
|
|
312
|
+
line: lineOf(text, String(entry.url || event)),
|
|
313
|
+
evidence: mask(String(entry.url || '')),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const url of Array.isArray(data.allowedHttpHookUrls) ? data.allowedHttpHookUrls : []) {
|
|
320
|
+
const value = String(url);
|
|
321
|
+
if (value === '*' || /^https?:\/\/\*/.test(value)) {
|
|
322
|
+
findings.push({
|
|
323
|
+
ruleId: 'agent.wildcard-hook-url',
|
|
324
|
+
description: 'Agent hooks may post to any host — a wildcard here is an exfiltration path',
|
|
325
|
+
severity: 'HIGH',
|
|
326
|
+
file: relPath,
|
|
327
|
+
line: lineOf(text, value),
|
|
328
|
+
evidence: value,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const env = data.env && typeof data.env === 'object' ? data.env : {};
|
|
334
|
+
for (const [key, value] of Object.entries(env)) {
|
|
335
|
+
const val = String(value);
|
|
336
|
+
if (val.length >= 8 && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val) &&
|
|
337
|
+
/(key|token|secret|password|credential)/i.test(key)) {
|
|
338
|
+
findings.push({
|
|
339
|
+
ruleId: 'agent.inline-secret',
|
|
340
|
+
description: `Agent settings hardcode a secret in env "${key}"`,
|
|
341
|
+
severity: 'CRITICAL',
|
|
342
|
+
file: relPath,
|
|
343
|
+
line: lineOf(text, key),
|
|
344
|
+
evidence: mask(val),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Files where a flag on a command line actually runs something. */
|
|
351
|
+
function isExecutableContext(relPath, base) {
|
|
352
|
+
const lower = base.toLowerCase();
|
|
353
|
+
if (lower === 'package.json' || lower === 'makefile' || lower === 'procfile') return true;
|
|
354
|
+
if (lower.startsWith('dockerfile') || lower.startsWith('docker-compose')) return true;
|
|
355
|
+
if (/\.(sh|bash|zsh|fish|bat|cmd|ps1)$/.test(lower)) return true;
|
|
356
|
+
const norm = relPath.replace(/\\/g, '/').toLowerCase();
|
|
357
|
+
if (norm.includes('.github/workflows/') || norm.includes('.gitlab-ci')) return true;
|
|
358
|
+
return isAgentSettingsFile(relPath, base);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const SKIP_PERMISSIONS_RE = /--dangerously-skip-permissions|--yolo\b|--dangerously-allow-browser/;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Reported only where the flag runs. Security notes and linters quote it, and flagging prose
|
|
365
|
+
* would make this loudest in the repositories that care most.
|
|
366
|
+
*/
|
|
367
|
+
function scanSkipPermissions(relPath, lines, findings) {
|
|
368
|
+
lines.forEach((line, i) => {
|
|
369
|
+
if (SKIP_PERMISSIONS_RE.test(line)) {
|
|
370
|
+
findings.push({
|
|
371
|
+
ruleId: 'agent.skip-permissions',
|
|
372
|
+
description: 'Permission prompts are disabled for an AI agent — destructive commands will not stop for review',
|
|
373
|
+
severity: 'CRITICAL',
|
|
374
|
+
file: relPath,
|
|
375
|
+
line: i + 1,
|
|
376
|
+
evidence: line.trim().slice(0, 180),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
223
382
|
function scanGithubWorkflow(relPath, text, findings) {
|
|
224
383
|
const lower = text.toLowerCase();
|
|
225
384
|
const hasPRTarget = /on:\s*[\s\S]*?pull_request_target/.test(lower) ||
|
|
@@ -426,6 +585,14 @@ function scanFile(absPath, relPath, findings, counters) {
|
|
|
426
585
|
scanMcpConfig(relPath, text, findings, jsonData);
|
|
427
586
|
}
|
|
428
587
|
|
|
588
|
+
if (isAgentSettingsFile(relPath, base)) {
|
|
589
|
+
scanAgentSettings(relPath, text, findings, jsonData);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (isExecutableContext(relPath, base)) {
|
|
593
|
+
scanSkipPermissions(relPath, lines, findings);
|
|
594
|
+
}
|
|
595
|
+
|
|
429
596
|
if (isInstructionFile(relPath, base)) {
|
|
430
597
|
flagInstructionFile(relPath, findings);
|
|
431
598
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entet/ai-agent-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scan a project for leaked secrets, risky MCP configs, and unsafe automation before handing it to an AI coding agent (Claude Code, Cursor, Codex). Zero dependencies, runs locally.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-agent-guard": "bin/ai-agent-guard.js"
|