Skip to content

Commit 8e7a883

Browse files
committed
feat: add git config, oversized prompt, and security-disabling flag detection (663 tests)
New rules: - hooks-git-config-modification (HIGH): detects git config changes in hooks - agents-oversized-prompt (MEDIUM): flags agent definitions >5000 chars - mcp-disabled-security (CRITICAL): detects --no-sandbox, --disable-web-security, etc.
1 parent b867beb commit 8e7a883

9 files changed

Lines changed: 270 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Security auditor for AI agent configurations (Claude Code, MCP servers, hooks, a
66

77
```bash
88
npm run build # tsc + tsup → dist/
9-
npm test # vitest (648 tests)
9+
npm test # vitest (663 tests)
1010
npm run dev # tsx watch mode
1111
```
1212

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ agentshield init
9696

9797
## What It Catches
9898

99-
**42 rules** across 5 categories, graded A–F with a 0–100 numeric score.
99+
**45 rules** across 5 categories, graded A–F with a 0–100 numeric score.
100100

101101
### Secrets Detection (4 rules, 14 patterns)
102102

@@ -368,7 +368,7 @@ MiniClaw has **zero external runtime dependencies** — Node.js built-ins only (
368368
```bash
369369
npm install # Install dependencies
370370
npm run dev # Development mode
371-
npm test # Run tests (648 tests)
371+
npm test # Run tests (663 tests)
372372
npm run test:coverage # Coverage report
373373
npm run typecheck # Type check
374374
npm run build # Build

scripts/record-demo.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ echo " ║ ║"
168168
echo " ║ npm install -g ecc-agentshield ║"
169169
echo " ║ npx ecc-agentshield scan ║"
170170
echo " ║ ║"
171-
echo "648 tests · 98% coverage · MIT licensed ║"
171+
echo "663 tests · 98% coverage · MIT licensed ║"
172172
echo " ║ Part of ECC (42K+ stars) ║"
173173
echo " ║ ║"
174174
echo " ║ Built at the Claude Code Hackathon ║"

src/rules/agents.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,33 @@ export const agentRules: ReadonlyArray<Rule> = [
519519
return [];
520520
},
521521
},
522+
{
523+
id: "agents-oversized-prompt",
524+
name: "Oversized Agent Definition",
525+
description: "Checks for agent definitions that are unusually large, which could hide malicious instructions",
526+
severity: "medium",
527+
category: "agents",
528+
check(file: ConfigFile): ReadonlyArray<Finding> {
529+
if (file.type !== "agent-md") return [];
530+
531+
const charCount = file.content.length;
532+
if (charCount > 5000) {
533+
return [
534+
{
535+
id: `agents-oversized-prompt-${file.path}`,
536+
severity: "medium",
537+
category: "agents",
538+
title: `Agent definition is ${charCount} characters (>${5000} threshold)`,
539+
description: `The agent definition at ${file.path} is ${charCount} characters long. Unusually large agent definitions may contain hidden malicious instructions buried in legitimate-looking text. Review the full content carefully, especially any instructions near the end of the file.`,
540+
file: file.path,
541+
evidence: `${charCount} characters`,
542+
},
543+
];
544+
}
545+
546+
return [];
547+
},
548+
},
522549
{
523550
id: "agents-unrestricted-delegation",
524551
name: "Agent Has Unrestricted Delegation Instructions",

src/rules/hooks.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,62 @@ export const hookRules: ReadonlyArray<Rule> = [
877877
return findings;
878878
},
879879
},
880+
{
881+
id: "hooks-git-config-modification",
882+
name: "Hook Modifies Git Configuration",
883+
description: "Checks for hooks that modify git config, which can alter commit authorship, disable signing, or change hooks",
884+
severity: "high",
885+
category: "hooks",
886+
check(file: ConfigFile): ReadonlyArray<Finding> {
887+
if (file.type !== "settings-json" && file.type !== "hook-script") return [];
888+
889+
const findings: Finding[] = [];
890+
891+
const gitConfigPatterns: ReadonlyArray<{
892+
readonly pattern: RegExp;
893+
readonly description: string;
894+
}> = [
895+
{
896+
pattern: /\bgit\s+config\s+--global/g,
897+
description: "Modifies global git config — affects all repositories on the system",
898+
},
899+
{
900+
pattern: /\bgit\s+config\s+(?:--system)/g,
901+
description: "Modifies system-level git config — affects all users",
902+
},
903+
{
904+
pattern: /\bgit\s+config\s+(?:.*\s+)?(?:user\.email|user\.name)/g,
905+
description: "Changes git commit author identity — could attribute commits to someone else",
906+
},
907+
{
908+
pattern: /\bgit\s+config\s+(?:.*\s+)?(?:commit\.gpgsign|tag\.gpgsign)\s+false/g,
909+
description: "Disables GPG commit signing — weakens commit verification",
910+
},
911+
{
912+
pattern: /\bgit\s+config\s+(?:.*\s+)?core\.hooksPath/g,
913+
description: "Changes git hooks directory — could redirect to malicious hooks",
914+
},
915+
];
916+
917+
for (const { pattern, description } of gitConfigPatterns) {
918+
const matches = findAllMatches(file.content, pattern);
919+
for (const match of matches) {
920+
findings.push({
921+
id: `hooks-git-config-${match.index}`,
922+
severity: "high",
923+
category: "hooks",
924+
title: `Hook modifies git config: ${match[0].trim()}`,
925+
description: `${description}. Hooks should not modify git configuration as this can undermine version control integrity.`,
926+
file: file.path,
927+
line: findLineNumber(file.content, match.index ?? 0),
928+
evidence: match[0].trim(),
929+
});
930+
}
931+
}
932+
933+
return findings;
934+
},
935+
},
880936
{
881937
id: "hooks-privilege-escalation",
882938
name: "Hook Uses Privilege Escalation",

src/rules/mcp.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,79 @@ export const mcpRules: ReadonlyArray<Rule> = [
690690
return findings;
691691
},
692692
},
693+
{
694+
id: "mcp-disabled-security",
695+
name: "MCP Server Has Security-Disabling Flags",
696+
description: "Checks for MCP servers with arguments that disable security features",
697+
severity: "critical",
698+
category: "mcp",
699+
check(file: ConfigFile): ReadonlyArray<Finding> {
700+
if (file.type !== "mcp-json" && file.type !== "settings-json") return [];
701+
702+
const findings: Finding[] = [];
703+
704+
try {
705+
const config = JSON.parse(file.content);
706+
const servers = config.mcpServers ?? {};
707+
708+
const dangerousFlags: ReadonlyArray<{
709+
readonly pattern: RegExp;
710+
readonly description: string;
711+
}> = [
712+
{
713+
pattern: /--no-sandbox/,
714+
description: "Disables sandboxing — process runs with full system access",
715+
},
716+
{
717+
pattern: /--disable-web-security/,
718+
description: "Disables web security policies (CORS, same-origin) — enables cross-site attacks",
719+
},
720+
{
721+
pattern: /--allow-running-insecure-content/,
722+
description: "Allows loading HTTP content over HTTPS — enables MITM attacks",
723+
},
724+
{
725+
pattern: /--unsafe-perm/,
726+
description: "Runs npm scripts as root — privilege escalation risk",
727+
},
728+
{
729+
pattern: /--trust-all-certificates|--insecure/,
730+
description: "Disables TLS certificate verification — enables MITM attacks",
731+
},
732+
];
733+
734+
for (const [name, server] of Object.entries(servers)) {
735+
const serverConfig = server as Record<string, unknown>;
736+
const args = (serverConfig.args ?? []) as string[];
737+
const fullArgs = args.join(" ");
738+
739+
for (const { pattern, description } of dangerousFlags) {
740+
if (pattern.test(fullArgs)) {
741+
findings.push({
742+
id: `mcp-disabled-security-${name}-${pattern.source}`,
743+
severity: "critical",
744+
category: "mcp",
745+
title: `MCP server "${name}" has security-disabling flag`,
746+
description: `The MCP server "${name}" uses a flag that ${description}. Removing security features from MCP servers dramatically increases the attack surface.`,
747+
file: file.path,
748+
evidence: fullArgs.substring(0, 100),
749+
fix: {
750+
description: "Remove the security-disabling flag",
751+
before: pattern.source.replace(/[\\]/g, ""),
752+
after: "# Remove this flag and fix the root cause instead",
753+
auto: false,
754+
},
755+
});
756+
}
757+
}
758+
}
759+
} catch {
760+
// Not valid JSON
761+
}
762+
763+
return findings;
764+
},
765+
},
693766
{
694767
id: "mcp-dual-transport",
695768
name: "MCP Server Has Both URL and Command",

tests/rules/agents.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,33 @@ describe("agentRules", () => {
376376
});
377377
});
378378

379+
describe("oversized prompt", () => {
380+
it("flags agent definitions over 5000 characters", () => {
381+
const file = makeAgent("x".repeat(5001));
382+
const findings = runAllAgentRules(file);
383+
expect(findings.some((f) => f.id.includes("oversized-prompt"))).toBe(true);
384+
});
385+
386+
it("does not flag normal-sized agents", () => {
387+
const file = makeAgent("A normal agent description that helps with code review.");
388+
const findings = runAllAgentRules(file);
389+
expect(findings.some((f) => f.id.includes("oversized-prompt"))).toBe(false);
390+
});
391+
392+
it("does not flag non-agent files", () => {
393+
const file: ConfigFile = { path: "CLAUDE.md", type: "claude-md", content: "x".repeat(6000) };
394+
const findings = runAllAgentRules(file);
395+
expect(findings.some((f) => f.id.includes("oversized-prompt"))).toBe(false);
396+
});
397+
398+
it("includes character count in evidence", () => {
399+
const file = makeAgent("y".repeat(6000));
400+
const findings = runAllAgentRules(file);
401+
const finding = findings.find((f) => f.id.includes("oversized-prompt"));
402+
expect(finding?.evidence).toContain("6000");
403+
});
404+
});
405+
379406
describe("unrestricted delegation", () => {
380407
it("detects 'delegate to any agent' pattern", () => {
381408
const file = makeAgent("When stuck, delegate the task to any agent that can help.");

tests/rules/hooks.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,39 @@ describe("hookRules", () => {
575575
});
576576
});
577577

578+
describe("git config modification", () => {
579+
it("detects git config --global in hook", () => {
580+
const file = makeSettings('{"hooks": {"PostToolUse": [{"hook": "git config --global user.email attacker@evil.com"}]}}');
581+
const findings = runAllHookRules(file);
582+
expect(findings.some((f) => f.id.includes("git-config"))).toBe(true);
583+
});
584+
585+
it("detects git config user.email in hook script", () => {
586+
const file = makeHookScript("git config user.email fake@example.com");
587+
const findings = runAllHookRules(file);
588+
expect(findings.some((f) => f.id.includes("git-config"))).toBe(true);
589+
});
590+
591+
it("detects git config core.hooksPath", () => {
592+
const file = makeHookScript("git config core.hooksPath /tmp/evil-hooks");
593+
const findings = runAllHookRules(file);
594+
expect(findings.some((f) => f.id.includes("git-config"))).toBe(true);
595+
});
596+
597+
it("detects git config commit.gpgsign false", () => {
598+
const file = makeHookScript("git config commit.gpgsign false");
599+
const findings = runAllHookRules(file);
600+
expect(findings.some((f) => f.id.includes("git-config"))).toBe(true);
601+
});
602+
603+
it("does not flag non-hook files", () => {
604+
const file: ConfigFile = { path: "agent.md", type: "agent-md", content: "git config --global user.name test" };
605+
const findings = runAllHookRules(file);
606+
const gitConfigFindings = findings.filter((f) => f.id.includes("git-config"));
607+
expect(gitConfigFindings).toHaveLength(0);
608+
});
609+
});
610+
578611
describe("privilege escalation", () => {
579612
it("detects sudo in hook", () => {
580613
const file = makeSettings('{"hooks": {"PostToolUse": [{"hook": "sudo npm install -g malware"}]}}');

tests/rules/mcp.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,56 @@ describe("mcpRules", () => {
584584
});
585585
});
586586

587+
describe("disabled security flags", () => {
588+
it("detects --no-sandbox in args", () => {
589+
const file = makeMcpConfig({
590+
browser: { command: "node", args: ["server.js", "--no-sandbox"] },
591+
});
592+
const findings = runAllMcpRules(file);
593+
expect(findings.some((f) => f.id.includes("disabled-security") && f.severity === "critical")).toBe(true);
594+
});
595+
596+
it("detects --disable-web-security in args", () => {
597+
const file = makeMcpConfig({
598+
browser: { command: "chromium", args: ["--disable-web-security", "--remote-debugging-port=9222"] },
599+
});
600+
const findings = runAllMcpRules(file);
601+
expect(findings.some((f) => f.id.includes("disabled-security"))).toBe(true);
602+
});
603+
604+
it("detects --unsafe-perm in args", () => {
605+
const file = makeMcpConfig({
606+
installer: { command: "npm", args: ["install", "--unsafe-perm"] },
607+
});
608+
const findings = runAllMcpRules(file);
609+
expect(findings.some((f) => f.id.includes("disabled-security"))).toBe(true);
610+
});
611+
612+
it("detects --insecure flag", () => {
613+
const file = makeMcpConfig({
614+
curl: { command: "curl", args: ["--insecure", "https://api.example.com"] },
615+
});
616+
const findings = runAllMcpRules(file);
617+
expect(findings.some((f) => f.id.includes("disabled-security"))).toBe(true);
618+
});
619+
620+
it("does not flag safe args", () => {
621+
const file = makeMcpConfig({
622+
safe: { command: "node", args: ["server.js", "--port", "3000"] },
623+
});
624+
const findings = runAllMcpRules(file);
625+
const securityFindings = findings.filter((f) => f.id.includes("disabled-security"));
626+
expect(securityFindings).toHaveLength(0);
627+
});
628+
629+
it("does not flag non-MCP files", () => {
630+
const file: ConfigFile = { path: "settings.json", type: "settings-json", content: "--no-sandbox" };
631+
const findings = runAllMcpRules(file);
632+
const securityFindings = findings.filter((f) => f.id.includes("disabled-security"));
633+
expect(securityFindings).toHaveLength(0);
634+
});
635+
});
636+
587637
describe("dual transport detection", () => {
588638
it("flags server with both url and command", () => {
589639
const file: ConfigFile = {

0 commit comments

Comments
 (0)