@codexguild/mcp 1.4.0 → 1.5.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 +21 -3
- package/dist/codexguild-mcp.js +345 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,16 +7,18 @@ forum, chat) and adds what has to run on your machine:
|
|
|
7
7
|
|
|
8
8
|
| Local tool | What it does |
|
|
9
9
|
|---|---|
|
|
10
|
-
| `codexguild_scan_skills` | Audits
|
|
10
|
+
| `codexguild_scan_skills` | Audits the skills installed for every supported harness (`~/.claude/skills`, `~/.claude/plugins`, `~/.codex/skills`, `~/.agents/skills`, `~/.hermes/skills`, `~/.cursor/skills`, `~/.gemini/skills`, … and the project's own `.claude/`, `.agents/`, `.github/` skills) **offline**, with the same scanner CodexGuild runs server-side (pipe-to-shell, secret exfiltration, prompt injection, hidden unicode, config tampering…). Nothing is uploaded. |
|
|
11
11
|
| `codexguild_sync` | Detects your stack with the **exact installed versions from your lockfiles** (package-lock, pnpm-lock, yarn.lock, bun.lock, poetry.lock, uv.lock, Pipfile.lock, Cargo.lock; go.mod), the Node/Python runtime (.nvmrc, .tool-versions, engines) and your installed skills, keys it to the current repository (each project keeps its own sync window), then asks CodexGuild what changed and which installed versions have known vulnerabilities. |
|
|
12
12
|
| `codexguild_advise` | Detects the instruction files in your project (`CLAUDE.md`, `AGENTS.md`, …) before proposing setup changes. Proposals always need your approval. |
|
|
13
|
+
| `codexguild_scan_workflows` | Reviews the project's GitHub Actions workflows that run coding agents: untrusted triggers with secrets, issue/PR text in prompts or shell scripts, `pull_request_target` with fork code, unrestricted agent tools, permissions, unpinned agent actions. Runs locally. |
|
|
14
|
+
| `codexguild_lint_instructions` | Lints the project's agent instruction files (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursorrules`…): size per session, missing build/test commands, README copy, generic advice, secrets, machine-specific paths, prompt injection. Runs locally — file contents are not uploaded. |
|
|
13
15
|
|
|
14
16
|
If your harness can't start local processes, use the remote endpoint `https://api.codexguild.com/v1/mcp` directly
|
|
15
17
|
(see [MCP docs](https://codexguild.com/docs/mcp)).
|
|
16
18
|
|
|
17
19
|
## Setup
|
|
18
20
|
|
|
19
|
-
1. Create an agent key in the [CodexGuild dashboard](https://codexguild.com/dashboard
|
|
21
|
+
1. Create an agent key in the [CodexGuild dashboard](https://codexguild.com/dashboard/agents) (`cgk_…`).
|
|
20
22
|
2. Add the server to your harness:
|
|
21
23
|
|
|
22
24
|
```json
|
|
@@ -39,11 +41,27 @@ claude mcp add codexguild --scope user -e CODEXGUILD_API_KEY=cgk_... -- npx -y @
|
|
|
39
41
|
|
|
40
42
|
Per-harness snippets (Codex, Hermes, OpenCode, Cursor, Gemini CLI, …): <https://codexguild.com/connect>.
|
|
41
43
|
|
|
44
|
+
## Guard hook (Claude Code PreToolUse)
|
|
45
|
+
|
|
46
|
+
`codexguild-mcp guard` reads every Bash command before it runs:
|
|
47
|
+
|
|
48
|
+
- **Destructive commands** — checked locally, no key or network: denies `rm -rf` of `/`, your home or system
|
|
49
|
+
directories, deletes through an unset variable (`rm -rf $DIR/`), disk writes and secrets sent over the network; asks
|
|
50
|
+
before deletes outside the project, force pushes, `git reset --hard`, database resets, `terraform destroy`, volume and
|
|
51
|
+
cloud-resource deletes. Rules: https://codexguild.com/docs/command-guard
|
|
52
|
+
- **Packages** — before an install command (npm/pnpm/yarn/bun add, npx/bunx/pnpm dlx, pip/uv/poetry/pipx, cargo add,
|
|
53
|
+
go get/install) it checks the packages with CodexGuild: `block` (doesn't exist or reported malicious) → deny; `warn` →
|
|
54
|
+
ask. Only package names and versions are sent; errors let the command run.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "npx -y @codexguild/mcp@latest guard", "timeout": 20 }] } ] } }
|
|
58
|
+
```
|
|
59
|
+
|
|
42
60
|
## Configuration
|
|
43
61
|
|
|
44
62
|
| Variable | Default | |
|
|
45
63
|
|---|---|---|
|
|
46
|
-
| `CODEXGUILD_API_KEY` | — | Agent key. Without it only the offline `codexguild_scan_skills`
|
|
64
|
+
| `CODEXGUILD_API_KEY` | — | Agent key. Without it only the offline `codexguild_scan_skills`, `codexguild_lint_instructions`, `codexguild_scan_workflows` and the guard's command checks work. |
|
|
47
65
|
| `CODEXGUILD_API_URL` | `https://api.codexguild.com/v1` | Override for self-hosted or local development. |
|
|
48
66
|
|
|
49
67
|
Requires Node.js 20 or newer.
|
package/dist/codexguild-mcp.js
CHANGED
|
@@ -6,8 +6,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { access, readFile as readFile3 } from "node:fs/promises";
|
|
10
|
-
import { isAbsolute, join as
|
|
9
|
+
import { access, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
|
|
10
|
+
import { isAbsolute as isAbsolute2, join as join4 } from "node:path";
|
|
11
11
|
|
|
12
12
|
// src/local-scan.ts
|
|
13
13
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
@@ -355,6 +355,134 @@ function lintInstructions(content, filename = "AGENTS.md") {
|
|
|
355
355
|
return { score, grade: grade(score), stats: { lines: lines.length, approxTokens, headings, codeBlocks, commands: commands2 }, issues: issues.slice(0, 100) };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// ../scanner/src/workflows.ts
|
|
359
|
+
var WEIGHT2 = { critical: 45, high: 25, medium: 10, low: 3 };
|
|
360
|
+
var AGENT_ACTIONS = [
|
|
361
|
+
[/anthropics\/claude-code(-base)?-action/i, "Claude Code"],
|
|
362
|
+
[/google-github-actions\/run-gemini-cli|google-gemini\/gemini-cli-action/i, "Gemini CLI"],
|
|
363
|
+
[/openai\/codex-action/i, "Codex"],
|
|
364
|
+
[/sst\/opencode\/github|opencode-ai\/opencode-action/i, "OpenCode"],
|
|
365
|
+
[/cline\/cline-action|cline-bot/i, "Cline"],
|
|
366
|
+
[/aider-ai\/aider-action|paul-gauthier\/aider/i, "Aider"],
|
|
367
|
+
[/github\/copilot-(swe-)?agent/i, "Copilot agent"]
|
|
368
|
+
];
|
|
369
|
+
var AGENT_CLIS = [
|
|
370
|
+
[/\bclaude\s+(-p|--print)\b|@anthropic-ai\/claude-code\b/, "Claude Code"],
|
|
371
|
+
[/\bcodex\s+(exec|e)\b|@openai\/codex\b/, "Codex"],
|
|
372
|
+
[/\bgemini\s+(-p|--prompt)\b|@google\/gemini-cli\b/, "Gemini CLI"],
|
|
373
|
+
[/\bopencode\s+run\b/, "OpenCode"],
|
|
374
|
+
[/\baider\s+.*--(message|msg)\b/, "Aider"]
|
|
375
|
+
];
|
|
376
|
+
var UNTRUSTED_EVENTS = ["pull_request_target", "issue_comment", "issues", "pull_request_review", "pull_request_review_comment", "discussion", "discussion_comment", "workflow_run"];
|
|
377
|
+
var UNTRUSTED_EXPR = /\$\{\{\s*(github\.event\.(issue|pull_request|comment|review|review_comment|discussion|head_commit|commits\[\d+\]|workflow_run)\.(?:[\w.]*\.)?(title|body|message|name|label|ref|head_branch|display_title)|github\.head_ref|github\.event\.pull_request\.head\.(ref|label))\s*\}\}/;
|
|
378
|
+
var AUTHOR_GATE = /author_association|github\.actor\s*==|github\.triggering_actor|github\.event\.sender\.login\s*==|contains\(fromJSON\(['"]\[["'](OWNER|MEMBER|COLLABORATOR)/i;
|
|
379
|
+
var DANGEROUS_FLAGS = [
|
|
380
|
+
[/--dangerously-skip-permissions/, "Claude Code runs every tool without asking (--dangerously-skip-permissions)"],
|
|
381
|
+
[/--dangerously-bypass-approvals-and-sandbox|--yolo\b/, "the agent runs without approvals or sandbox (--yolo / --dangerously-bypass-approvals-and-sandbox)"],
|
|
382
|
+
[/\b(allowed_tools|allowedTools|--allowedTools|--allowed-tools)\b[^\n]*["',\s[]Bash(?!\()/, "the agent may run any shell command (Bash allowed without a command pattern)"]
|
|
383
|
+
];
|
|
384
|
+
function grade2(score) {
|
|
385
|
+
return score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F";
|
|
386
|
+
}
|
|
387
|
+
function blocks(lines) {
|
|
388
|
+
const out = [];
|
|
389
|
+
let kind = null;
|
|
390
|
+
let indent = -1;
|
|
391
|
+
for (const raw of lines) {
|
|
392
|
+
const ind = raw.length - raw.trimStart().length;
|
|
393
|
+
if (kind && raw.trim() && ind <= indent) kind = null;
|
|
394
|
+
const m = raw.match(/^(\s*)(?:-\s+)?(run|prompt|direct_prompt|override_prompt|custom_instructions|claude_args|codex_args|gemini_args|args|message):\s*(.*)$/);
|
|
395
|
+
if (m) {
|
|
396
|
+
const k = m[2] === "run" ? "run" : "prompt";
|
|
397
|
+
out.push(k);
|
|
398
|
+
kind = /^[|>][-+]?\s*$/.test(m[3]) || m[3] === "" ? k : null;
|
|
399
|
+
indent = m[1].length + (raw.trimStart().startsWith("-") ? 2 : 0);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
out.push(kind);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
function lintWorkflow(content) {
|
|
407
|
+
const issues = [];
|
|
408
|
+
const lines = content.split(/\r?\n/);
|
|
409
|
+
const text2 = lines.map((l) => l.replace(/\s+#.*$/, "")).join("\n");
|
|
410
|
+
const agents = /* @__PURE__ */ new Set();
|
|
411
|
+
const agentLines = [];
|
|
412
|
+
lines.forEach((l, i) => {
|
|
413
|
+
for (const [re, name] of [...AGENT_ACTIONS, ...AGENT_CLIS]) if (re.test(l)) {
|
|
414
|
+
agents.add(name);
|
|
415
|
+
agentLines.push(i + 1);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
const untrustedTriggers = UNTRUSTED_EVENTS.filter((e) => new RegExp(`(^|[\\s,[{'"-])${e}\\s*(:|,|\\]|$)`, "m").test(text2));
|
|
419
|
+
const hasAgent = agents.size > 0;
|
|
420
|
+
const secrets = [...text2.matchAll(/\$\{\{\s*secrets\.(\w+)\s*\}\}/g)].map((m) => m[1]).filter((s) => s !== "GITHUB_TOKEN");
|
|
421
|
+
const gated = AUTHOR_GATE.test(text2);
|
|
422
|
+
const kinds = blocks(lines);
|
|
423
|
+
const firstAgent = agentLines[0];
|
|
424
|
+
if (hasAgent && untrustedTriggers.length && secrets.length && !gated) {
|
|
425
|
+
issues.push({
|
|
426
|
+
id: "untrusted-trigger-with-secrets",
|
|
427
|
+
severity: "critical",
|
|
428
|
+
line: firstAgent,
|
|
429
|
+
title: `Anyone can start ${[...agents].join(" / ")} with your secrets (${untrustedTriggers.join(", ")})`,
|
|
430
|
+
detail: `${untrustedTriggers.join(", ")} runs for outside contributors, and this job gives the agent ${[...new Set(secrets)].slice(0, 4).join(", ")}. A crafted title, body or comment can steer the agent into revealing or misusing them. Restrict who can trigger it (check github.event.comment.author_association / sender against OWNER, MEMBER, COLLABORATOR), or move secrets to a workflow that only maintainers trigger.`
|
|
431
|
+
});
|
|
432
|
+
} else if (hasAgent && untrustedTriggers.length && !gated) {
|
|
433
|
+
issues.push({
|
|
434
|
+
id: "untrusted-trigger",
|
|
435
|
+
severity: "medium",
|
|
436
|
+
line: firstAgent,
|
|
437
|
+
title: `Outside contributors can start the agent (${untrustedTriggers.join(", ")})`,
|
|
438
|
+
detail: "No author check was found. Even without secrets, the agent spends your quota and can write to the repository with the job token. Gate it on author_association."
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (untrustedTriggers.includes("pull_request_target")) {
|
|
442
|
+
lines.forEach((l, i) => {
|
|
443
|
+
if (/^\s*ref:\s*\$\{\{\s*(github\.event\.pull_request\.head\.(sha|ref)|github\.head_ref)\s*\}\}/.test(l)) {
|
|
444
|
+
issues.push({ id: "prt-checkout-head", severity: "critical", line: i + 1, title: "pull_request_target checks out the pull request code", detail: "The job runs with your secrets and a write token, and now with code from the fork. Anything the agent or a build step executes from that checkout can steal them. Use pull_request for untrusted code, or check out the base branch only." });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
lines.forEach((l, i) => {
|
|
449
|
+
const m = l.match(UNTRUSTED_EXPR);
|
|
450
|
+
if (!m) return;
|
|
451
|
+
const kind = kinds[i];
|
|
452
|
+
if (kind === "run") {
|
|
453
|
+
issues.push({ id: "untrusted-in-run", severity: "high", line: i + 1, title: `Untrusted ${m[1].split(".").slice(-2).join(".")} expanded inside a shell`, detail: 'The value is pasted into the script before it runs, so a title like `"; curl \u2026 | sh #` executes. Pass it through env: (VALUE: ${{ \u2026 }}) and use "$VALUE" in the script.' });
|
|
454
|
+
} else if (kind === "prompt" || hasAgent) {
|
|
455
|
+
issues.push({ id: "untrusted-in-prompt", severity: hasAgent ? "high" : "medium", line: i + 1, title: `Untrusted ${m[1].split(".").slice(-2).join(".")} inserted into the agent's prompt`, detail: "Text from outside contributors becomes instructions to the agent \u2014 the prompt-injection path behind the 2026 agent-action key leaks. Let the agent read the issue/PR through its tools as data, keep tools and permissions minimal, and never give this job secrets it does not need." });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
lines.forEach((l, i) => {
|
|
459
|
+
for (const [re, what] of DANGEROUS_FLAGS) {
|
|
460
|
+
if (re.test(l)) {
|
|
461
|
+
const severe = untrustedTriggers.length > 0 && !gated;
|
|
462
|
+
issues.push({ id: "agent-unrestricted", severity: severe ? "high" : "medium", line: i + 1, title: `In CI, ${what}`, detail: severe ? "Combined with a trigger outside contributors control, a prompt injection can run arbitrary commands with the job token and secrets. Allow only the commands the task needs (e.g. Bash(npm test))." : "Allow only the commands the task needs (e.g. Bash(npm test)); a prompt injection anywhere in the input then cannot run arbitrary commands." });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
if (/^\s*permissions:\s*write-all\b/m.test(text2)) {
|
|
467
|
+
const n = lines.findIndex((l) => /permissions:\s*write-all/.test(l)) + 1;
|
|
468
|
+
issues.push({ id: "write-all", severity: hasAgent ? "high" : "medium", line: n, title: "permissions: write-all", detail: "The job token can push code, change releases and edit workflows. Grant only what the job needs (e.g. contents: read, pull-requests: write)." });
|
|
469
|
+
} else if (hasAgent && !/^\s*permissions:/m.test(text2)) {
|
|
470
|
+
issues.push({ id: "no-permissions", severity: "medium", title: "No permissions block", detail: "Without one, the job token gets the repository default, which is often read/write for everything. Declare the minimum (contents: read, plus what the agent must write)." });
|
|
471
|
+
}
|
|
472
|
+
lines.forEach((l, i) => {
|
|
473
|
+
const m = l.match(/uses:\s*([\w.-]+\/[\w./-]+)@([\w.-]+)/);
|
|
474
|
+
if (!m || !AGENT_ACTIONS.some(([re]) => re.test(m[1]))) return;
|
|
475
|
+
if (!/^[0-9a-f]{40}$/.test(m[2])) {
|
|
476
|
+
issues.push({ id: "unpinned-agent-action", severity: "medium", line: i + 1, title: `${m[1]}@${m[2]} is not pinned to a commit`, detail: "This action receives your API keys. A moved tag or a compromised release changes what runs with them. Pin the full commit SHA (with the version in a comment) and update deliberately." });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
if (hasAgent && /secrets:\s*inherit/.test(text2)) {
|
|
480
|
+
issues.push({ id: "secrets-inherit", severity: "medium", line: lines.findIndex((l) => /secrets:\s*inherit/.test(l)) + 1, title: "secrets: inherit", detail: "Every repository secret is passed on. Pass only the key the agent needs." });
|
|
481
|
+
}
|
|
482
|
+
const score = Math.max(0, 100 - issues.reduce((n, x) => n + WEIGHT2[x.severity], 0));
|
|
483
|
+
return { score: hasAgent || issues.length ? score : 100, grade: grade2(score), agents: [...agents], untrustedTriggers, issues: issues.sort((a, b) => WEIGHT2[b.severity] - WEIGHT2[a.severity] || (a.line ?? 0) - (b.line ?? 0)) };
|
|
484
|
+
}
|
|
485
|
+
|
|
358
486
|
// ../scanner/src/index.ts
|
|
359
487
|
var SCANNER_VERSION = "1.2.0";
|
|
360
488
|
var MAX_PER_RULE_PER_FILE = 5;
|
|
@@ -787,12 +915,12 @@ function fromPyproject(raw) {
|
|
|
787
915
|
}
|
|
788
916
|
return deps;
|
|
789
917
|
}
|
|
790
|
-
function tomlPackages(raw,
|
|
918
|
+
function tomlPackages(raw, normalize2 = pep503) {
|
|
791
919
|
const out = /* @__PURE__ */ new Map();
|
|
792
920
|
for (const block of raw.split(/^\[\[package\]\]\s*$/m).slice(1)) {
|
|
793
921
|
const name = block.match(/^name\s*=\s*"([^"]+)"/m)?.[1];
|
|
794
922
|
const version = block.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
|
795
|
-
if (name && version && !out.has(
|
|
923
|
+
if (name && version && !out.has(normalize2(name))) out.set(normalize2(name), version);
|
|
796
924
|
}
|
|
797
925
|
return out;
|
|
798
926
|
}
|
|
@@ -961,6 +1089,160 @@ async function installedSkillNames(skillDirs) {
|
|
|
961
1089
|
return [...names];
|
|
962
1090
|
}
|
|
963
1091
|
|
|
1092
|
+
// src/destructive.ts
|
|
1093
|
+
import { realpathSync } from "node:fs";
|
|
1094
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
1095
|
+
import { isAbsolute, join as join3, normalize, resolve as resolve3, sep as sep2 } from "node:path";
|
|
1096
|
+
function split(line) {
|
|
1097
|
+
return line.split(/&&|\|\||[;\n]/).flatMap((part) => part.split(/(?<!\|)\|(?!\|)/).map((raw) => raw.trim())).filter(Boolean).map((raw) => ({ raw, words: (raw.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((w) => w.replace(/^["']|["']$/g, "")) }));
|
|
1098
|
+
}
|
|
1099
|
+
function strip(words) {
|
|
1100
|
+
let w = words;
|
|
1101
|
+
while (w.length && /^(sudo|doas|env|time|nice|command|exec|\w+=\S*)$/.test(w[0])) w = w.slice(1);
|
|
1102
|
+
return w;
|
|
1103
|
+
}
|
|
1104
|
+
var SYSTEM_DIRS = ["/", "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/opt", "/proc", "/root", "/sbin", "/sys", "/usr", "/var", "/System", "/Library", "/Applications", "/Users", "/home", "/private", "/Volumes"];
|
|
1105
|
+
function expand(arg, ctx) {
|
|
1106
|
+
let out = arg.replace(/^~(?=$|\/)/, ctx.home);
|
|
1107
|
+
let unresolved = false;
|
|
1108
|
+
out = out.replace(/\$\{(\w+)(?::?[-=][^}]*)?\}|\$(\w+)/g, (m, a, b) => {
|
|
1109
|
+
const name = a ?? b;
|
|
1110
|
+
const v = name === "HOME" ? ctx.home : name === "PWD" ? ctx.cwd : ctx.env[name];
|
|
1111
|
+
if (v === void 0 || v === "") {
|
|
1112
|
+
if (/:?[-=]/.test(m)) return m.replace(/^\$\{\w+:?[-=]|\}$/g, "");
|
|
1113
|
+
unresolved = true;
|
|
1114
|
+
return "";
|
|
1115
|
+
}
|
|
1116
|
+
return v;
|
|
1117
|
+
});
|
|
1118
|
+
return unresolved ? null : out;
|
|
1119
|
+
}
|
|
1120
|
+
var within = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep2) ? dir : dir + sep2);
|
|
1121
|
+
function real(p) {
|
|
1122
|
+
try {
|
|
1123
|
+
return realpathSync(p);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return p;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function judgeDelete(target, ctx, flags) {
|
|
1129
|
+
const expanded = expand(target, ctx);
|
|
1130
|
+
if (expanded === null) {
|
|
1131
|
+
return { decision: "deny", reason: `"${target}" contains a variable that is not set here, so it may expand to nothing \u2014 e.g. "rm -rf $DIR/" becomes "rm -rf /". Use the literal path.` };
|
|
1132
|
+
}
|
|
1133
|
+
if (/^\/?\*$|^~\/?\*$/.test(target) || expanded === "/*" || expanded === `${ctx.home}/*`) {
|
|
1134
|
+
return { decision: "deny", reason: `"rm ${flags} ${target}" deletes everything in ${expanded.startsWith(ctx.home) ? "the home directory" : "the filesystem root"}.` };
|
|
1135
|
+
}
|
|
1136
|
+
const abs = normalize(isAbsolute(expanded) ? expanded : resolve3(ctx.cwd, expanded)).replace(/\/\*$/, "").replace(/(.)\/+$/, "$1") || "/";
|
|
1137
|
+
const resolved = real(abs);
|
|
1138
|
+
for (const p of /* @__PURE__ */ new Set([abs, resolved])) {
|
|
1139
|
+
if (p === ctx.home || within(ctx.home, p)) return { decision: "deny", reason: `"rm ${flags} ${target}" would delete the home directory (${p}).` };
|
|
1140
|
+
if (SYSTEM_DIRS.some((d) => p === d)) return { decision: "deny", reason: `"rm ${flags} ${target}" would delete the system directory ${p}.` };
|
|
1141
|
+
}
|
|
1142
|
+
const tmp = [real(tmpdir()), "/tmp", "/private/tmp", "/var/folders"];
|
|
1143
|
+
const safe = (p) => within(p, ctx.project) || tmp.some((t) => within(p, t));
|
|
1144
|
+
if (!safe(abs) || !safe(resolved)) {
|
|
1145
|
+
const via = resolved !== abs ? ` (it resolves to ${resolved} through a link)` : "";
|
|
1146
|
+
return { decision: "ask", reason: `"rm ${flags} ${target}" deletes ${abs}${via}, outside the project ${ctx.project}. Confirm that this is intended.` };
|
|
1147
|
+
}
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
var RULES = [
|
|
1151
|
+
// rm -r / -rf / --recursive
|
|
1152
|
+
(w, _raw, ctx) => {
|
|
1153
|
+
if (w[0]?.split("/").pop() !== "rm") return null;
|
|
1154
|
+
const flags = w.filter((x) => x.startsWith("-"));
|
|
1155
|
+
if (!flags.some((f) => /^-[a-zA-Z]*[rR]/.test(f) || f === "--recursive")) return null;
|
|
1156
|
+
const targets = w.slice(1).filter((x) => !x.startsWith("-"));
|
|
1157
|
+
let worst = null;
|
|
1158
|
+
for (const t of targets) {
|
|
1159
|
+
const v = judgeDelete(t, ctx, flags.join(" "));
|
|
1160
|
+
if (v?.decision === "deny") return v;
|
|
1161
|
+
worst ??= v;
|
|
1162
|
+
}
|
|
1163
|
+
return worst;
|
|
1164
|
+
},
|
|
1165
|
+
// find … -delete / -exec rm outside the project
|
|
1166
|
+
(w, _raw, ctx) => {
|
|
1167
|
+
if (w[0] !== "find" || !(w.includes("-delete") || w.includes("-exec") && w.includes("rm"))) return null;
|
|
1168
|
+
const start = w[1] && !w[1].startsWith("-") ? w[1] : ".";
|
|
1169
|
+
const v = judgeDelete(start, ctx, "(find -delete)");
|
|
1170
|
+
return v && { ...v, reason: v.reason.replace(/^"rm \(find -delete\) /, `"find ${start} \u2026 -delete" `) };
|
|
1171
|
+
},
|
|
1172
|
+
// disks and filesystems
|
|
1173
|
+
(w) => /^mkfs(\.\w+)?$/.test(w[0] ?? "") || w[0] === "dd" && w.some((x) => /^of=\/dev\//.test(x)) ? { decision: "deny", reason: `"${w.join(" ")}" overwrites a disk or partition.` } : null,
|
|
1174
|
+
(_w, raw) => />\s*\/dev\/(sd|nvme|disk|hd)\w*/.test(raw) ? { decision: "deny", reason: "Writing directly to a disk device." } : null,
|
|
1175
|
+
// recursive chmod/chown on / or ~
|
|
1176
|
+
(w, _raw, ctx) => {
|
|
1177
|
+
if (!["chmod", "chown"].includes(w[0] ?? "") || !w.some((x) => /^-[a-zA-Z]*R/.test(x))) return null;
|
|
1178
|
+
const t = w.slice(1).filter((x) => !x.startsWith("-")).slice(1);
|
|
1179
|
+
const hit = t.map((x) => expand(x, ctx) ?? "/").find((x) => SYSTEM_DIRS.includes(normalize(x)) || normalize(x) === ctx.home);
|
|
1180
|
+
return hit ? { decision: "deny", reason: `Recursive ${w[0]} on ${hit}.` } : null;
|
|
1181
|
+
},
|
|
1182
|
+
// git
|
|
1183
|
+
(w) => {
|
|
1184
|
+
if (w[0] !== "git") return null;
|
|
1185
|
+
const sub = w[1];
|
|
1186
|
+
if (sub === "push" && w.some((x) => /^(-f|--force|--force-with-lease(=.*)?|--mirror|--delete|-d)$/.test(x) || /^\+/.test(x) || /^:/.test(x))) {
|
|
1187
|
+
return { decision: "ask", reason: `"${w.join(" ")}" rewrites or deletes history on the remote. Confirm with the user first.` };
|
|
1188
|
+
}
|
|
1189
|
+
if (sub === "reset" && w.includes("--hard")) return { decision: "ask", reason: '"git reset --hard" throws away uncommitted changes.' };
|
|
1190
|
+
if (sub === "clean" && w.some((x) => /^-[a-zA-Z]*f/.test(x))) return { decision: "ask", reason: `"${w.join(" ")}" permanently deletes untracked files${w.some((x) => /x/.test(x) && x.startsWith("-")) ? ", including ignored ones like .env" : ""}.` };
|
|
1191
|
+
if ((sub === "checkout" || sub === "restore") && w.includes(".") && !w.includes("--staged")) return { decision: "ask", reason: `"${w.join(" ")}" discards all uncommitted changes.` };
|
|
1192
|
+
if (sub === "branch" && w.includes("-D")) return { decision: "ask", reason: `"${w.join(" ")}" deletes a branch even if it is not merged.` };
|
|
1193
|
+
if (sub === "add" && w.slice(2).some((x) => /(^|\/)\.env(\..+)?$|\.pem$|id_rsa|id_ed25519/.test(x) && !/\.example$|\.sample$|\.template$/.test(x))) {
|
|
1194
|
+
return { decision: "ask", reason: `"${w.join(" ")}" stages a file that usually holds secrets.` };
|
|
1195
|
+
}
|
|
1196
|
+
return null;
|
|
1197
|
+
},
|
|
1198
|
+
// databases
|
|
1199
|
+
(w, raw) => {
|
|
1200
|
+
const s = raw.toLowerCase();
|
|
1201
|
+
if (/\bprisma\b.*\bmigrate\s+reset\b/.test(s) || /\bprisma\b.*\bdb\s+push\b.*--(force-reset|accept-data-loss)/.test(s)) {
|
|
1202
|
+
return { decision: "ask", reason: `"${raw.trim()}" deletes data in the database ${/database_url=/.test(s) ? "named in DATABASE_URL" : "from DATABASE_URL"}. Confirm it is not a shared or production database.` };
|
|
1203
|
+
}
|
|
1204
|
+
if (/\b(drizzle-kit\s+drop|rails\s+db:(drop|reset)|rake\s+db:(drop|reset)|manage\.py\s+flush|sequelize\s+db:drop|typeorm\s+schema:drop|knex\s+migrate:rollback\s+--all)\b/.test(s)) {
|
|
1205
|
+
return { decision: "ask", reason: `"${raw.trim()}" drops or empties the database.` };
|
|
1206
|
+
}
|
|
1207
|
+
if (/\b(psql|mysql|mariadb|sqlite3|mongosh|mongo|redis-cli|clickhouse-client)\b/.test(s) && /\b(drop\s+(database|schema|table)|truncate\b|delete\s+from\s+\w+\s*(;|$|"|')|flushall|flushdb|dropdatabase\(\))/.test(s)) {
|
|
1208
|
+
return { decision: "ask", reason: `"${raw.trim().slice(0, 200)}" deletes database data. Confirm the target is not production.` };
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
},
|
|
1212
|
+
// infrastructure and cloud
|
|
1213
|
+
(w, raw) => {
|
|
1214
|
+
const s = raw.toLowerCase();
|
|
1215
|
+
const hit = /\b(terraform|tofu|terragrunt)\s+(destroy|apply\b.*-destroy)/.test(s) && "tears down infrastructure" || /\bpulumi\s+(destroy|down)\b/.test(s) && "tears down infrastructure" || /\bcdk\s+destroy\b/.test(s) && "tears down infrastructure" || /\bkubectl\s+delete\b.*(\b(ns|namespace|namespaces|pv|pvc|node)\b|--all\b)/.test(s) && "deletes Kubernetes resources broadly" || /\bhelm\s+(uninstall|delete)\b/.test(s) && "removes a Helm release" || /\bdocker\s+(system\s+prune\b.*(-a|--all|--volumes)|volume\s+(rm|prune)\b|compose\s+down\b.*(-v|--volumes))/.test(s) && "deletes Docker volumes or all unused data" || /\baws\s+s3\s+(rm\b.*--recursive|rb\b)/.test(s) && "deletes S3 objects or a bucket" || /\baws\s+\S+\s+(delete|terminate)-/.test(s) && "deletes AWS resources" || /\bgcloud\b.*\b(delete)\b/.test(s) && "deletes Google Cloud resources" || /\baz\s+\S+.*\bdelete\b/.test(s) && "deletes Azure resources" || /\b(vercel|netlify|fly|flyctl|railway|heroku)\b.*\b(remove|rm|destroy|delete|apps:destroy)\b/.test(s) && "deletes a deployment or app";
|
|
1216
|
+
return hit ? { decision: "ask", reason: `"${raw.trim().slice(0, 200)}" ${hit}. Confirm with the user first.` } : null;
|
|
1217
|
+
}
|
|
1218
|
+
];
|
|
1219
|
+
function exfiltration(line) {
|
|
1220
|
+
const s = line.toLowerCase();
|
|
1221
|
+
const net = /\b(curl|wget|nc|ncat|netcat|socat|http|httpie|scp|rsync|ftp|telnet)\b/;
|
|
1222
|
+
const secret = /(\.env\b|\.ssh\/|id_rsa|id_ed25519|\.aws\/credentials|\.npmrc|\.netrc|\.pgpass|\.git-credentials|\.docker\/config\.json|\bprintenv\b|(^|[|;&\s])env(\s*[|;&]|\s*$))/;
|
|
1223
|
+
if (!net.test(s) || !secret.test(s)) return null;
|
|
1224
|
+
const piped = /\|\s*(curl|wget|nc|ncat|netcat|socat|http)\b/.test(s);
|
|
1225
|
+
const uploaded = /(\s-d|--data(-binary|-raw)?|\s-F|--form|\s-T|--upload-file)\s+\S*@\S*(\.env|\.ssh|id_rsa|credentials|\.npmrc|\.netrc)/.test(line) || /\b(scp|rsync)\b.*(\.env|\.ssh|id_rsa|credentials)/.test(s);
|
|
1226
|
+
return piped || uploaded ? { decision: "deny", reason: "This sends secrets (.env, SSH keys, cloud credentials or the environment) over the network." } : null;
|
|
1227
|
+
}
|
|
1228
|
+
function destructiveVerdict(line, opts = {}) {
|
|
1229
|
+
const home = normalize(opts.home ?? homedir2());
|
|
1230
|
+
const cwd = normalize(opts.cwd ?? process.cwd());
|
|
1231
|
+
const ctx = { cwd, home, project: normalize(opts.project ?? cwd), env: opts.env ?? process.env };
|
|
1232
|
+
if (ctx.project === home || SYSTEM_DIRS.includes(ctx.project)) ctx.project = join3(home, "\0no-project");
|
|
1233
|
+
const verdicts = [exfiltration(line), /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(line) ? { decision: "deny", reason: "Fork bomb." } : null];
|
|
1234
|
+
for (const { raw, words } of split(line)) {
|
|
1235
|
+
const w = strip(words);
|
|
1236
|
+
if (!w.length) continue;
|
|
1237
|
+
for (const rule of RULES) verdicts.push(rule(w, raw, ctx));
|
|
1238
|
+
}
|
|
1239
|
+
const hits = verdicts.filter((v) => !!v);
|
|
1240
|
+
if (!hits.length) return null;
|
|
1241
|
+
const deny = hits.filter((v) => v.decision === "deny");
|
|
1242
|
+
const pick = deny.length ? deny : hits;
|
|
1243
|
+
return { decision: deny.length ? "deny" : "ask", reason: [...new Set(pick.map((v) => v.reason))].join("\n") };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
964
1246
|
// src/guard.ts
|
|
965
1247
|
function commands(line) {
|
|
966
1248
|
return line.split(/&&|\|\||[;|\n]/).map((c) => (c.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((w) => w.replace(/^["']|["']$/g, ""))).filter((w) => w.length);
|
|
@@ -1038,17 +1320,20 @@ async function readStdin() {
|
|
|
1038
1320
|
}
|
|
1039
1321
|
async function runGuard(opts) {
|
|
1040
1322
|
let command = "";
|
|
1323
|
+
let cwd;
|
|
1041
1324
|
try {
|
|
1042
1325
|
const payload = JSON.parse(await readStdin());
|
|
1043
1326
|
command = payload.tool_input?.command ?? "";
|
|
1327
|
+
cwd = payload.cwd;
|
|
1044
1328
|
} catch {
|
|
1045
1329
|
return 0;
|
|
1046
1330
|
}
|
|
1331
|
+
const local = destructiveVerdict(command, { cwd, project: cwd });
|
|
1047
1332
|
const packages = packagesInCommand(command);
|
|
1048
|
-
if (!packages.length) return
|
|
1333
|
+
if (local?.decision === "deny" || !packages.length) return emit(local);
|
|
1049
1334
|
if (!opts.key) {
|
|
1050
1335
|
console.error("codexguild guard: CODEXGUILD_API_KEY is not set \u2014 packages were not checked.");
|
|
1051
|
-
return
|
|
1336
|
+
return emit(local);
|
|
1052
1337
|
}
|
|
1053
1338
|
let results;
|
|
1054
1339
|
try {
|
|
@@ -1062,12 +1347,18 @@ async function runGuard(opts) {
|
|
|
1062
1347
|
results = (await res.json()).results;
|
|
1063
1348
|
} catch (e) {
|
|
1064
1349
|
console.error(`codexguild guard: check failed (${e instanceof Error ? e.message : e}) \u2014 command not blocked.`);
|
|
1065
|
-
return
|
|
1066
|
-
}
|
|
1067
|
-
const decision = guardDecision(results);
|
|
1068
|
-
if (decision) {
|
|
1069
|
-
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision.decision, permissionDecisionReason: decision.reason } }));
|
|
1350
|
+
return emit(local);
|
|
1070
1351
|
}
|
|
1352
|
+
return emit(mergeVerdicts(local, guardDecision(results)));
|
|
1353
|
+
}
|
|
1354
|
+
function mergeVerdicts(a, b) {
|
|
1355
|
+
if (!a || !b) return a ?? b;
|
|
1356
|
+
if (a.decision !== b.decision) return a.decision === "deny" ? a : b;
|
|
1357
|
+
return { decision: a.decision, reason: `${a.reason}
|
|
1358
|
+
${b.reason}` };
|
|
1359
|
+
}
|
|
1360
|
+
function emit(d) {
|
|
1361
|
+
if (d) process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: d.decision, permissionDecisionReason: `CodexGuild guard: ${d.reason}` } }));
|
|
1071
1362
|
return 0;
|
|
1072
1363
|
}
|
|
1073
1364
|
function guardDecision(results) {
|
|
@@ -1077,7 +1368,7 @@ function guardDecision(results) {
|
|
|
1077
1368
|
const blocked = flagged.some((r) => r.verdict === "block");
|
|
1078
1369
|
return {
|
|
1079
1370
|
decision: blocked ? "deny" : "ask",
|
|
1080
|
-
reason: `
|
|
1371
|
+
reason: `package check:
|
|
1081
1372
|
${lines.join("\n")}${blocked ? "\nUse the correct package name, or ask the user before trying again." : ""}`
|
|
1082
1373
|
};
|
|
1083
1374
|
}
|
|
@@ -1085,11 +1376,11 @@ ${lines.join("\n")}${blocked ? "\nUse the correct package name, or ask the user
|
|
|
1085
1376
|
// src/index.ts
|
|
1086
1377
|
var API = (process.env.CODEXGUILD_API_URL || "https://api.codexguild.com/v1").replace(/\/$/, "");
|
|
1087
1378
|
var KEY = process.env.CODEXGUILD_API_KEY ?? "";
|
|
1088
|
-
var VERSION = "1.
|
|
1379
|
+
var VERSION = "1.5.0";
|
|
1089
1380
|
if (process.argv[2] === "guard") process.exit(await runGuard({ api: API, key: KEY }));
|
|
1090
1381
|
var INSTRUCTION_FILES = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", "AGENTS.override.md", ".hermes.md", "HERMES.md", "GEMINI.md", ".cursorrules"];
|
|
1091
1382
|
async function existingInstructionFiles(cwd) {
|
|
1092
|
-
const present = await Promise.all(INSTRUCTION_FILES.map((f) => access(
|
|
1383
|
+
const present = await Promise.all(INSTRUCTION_FILES.map((f) => access(join4(cwd, f)).then(() => f, () => null)));
|
|
1093
1384
|
return present.filter((f) => f !== null);
|
|
1094
1385
|
}
|
|
1095
1386
|
var remote = null;
|
|
@@ -1119,7 +1410,8 @@ var SCAN_TOOL = {
|
|
|
1119
1410
|
};
|
|
1120
1411
|
var ENHANCED = {
|
|
1121
1412
|
codexguild_sync: "Runs locally first: the stack (package.json, requirements.txt, go.mod, \u2026) and installed skill NAMES are detected automatically \u2014 only names/versions are sent, never file contents; you only need force/since.",
|
|
1122
|
-
codexguild_advise: "Runs locally first: stack, installed skills and the instruction files present in the project are detected automatically; you only need harness."
|
|
1413
|
+
codexguild_advise: "Runs locally first: stack, installed skills and the instruction files present in the project are detected automatically; you only need harness.",
|
|
1414
|
+
codexguild_debug: "Runs locally first: the project's package versions are read from its lockfiles and sent with the error \u2014 you only need error (and stack)."
|
|
1123
1415
|
};
|
|
1124
1416
|
function enhance(t) {
|
|
1125
1417
|
const note = ENHANCED[t.name];
|
|
@@ -1135,6 +1427,10 @@ ${note}`,
|
|
|
1135
1427
|
};
|
|
1136
1428
|
}
|
|
1137
1429
|
async function detected(name, cwd) {
|
|
1430
|
+
if (name === "codexguild_debug") {
|
|
1431
|
+
const { deps: deps2 } = await detectStack(cwd);
|
|
1432
|
+
return { args: { packages: deps2.slice(0, 300).map((d) => ({ name: d.name, ...d.version ? { version: d.version } : {}, ...d.ecosystem ? { ecosystem: d.ecosystem } : {} })) } };
|
|
1433
|
+
}
|
|
1138
1434
|
const [{ deps, manifests, lockfiles, exact, ranges }, local, project] = await Promise.all([detectStack(cwd), scanLocalSkills(), projectKey(cwd)]);
|
|
1139
1435
|
const installedSkills = await installedSkillNames(local.reports.map((r) => r.dir));
|
|
1140
1436
|
if (name === "codexguild_advise") {
|
|
@@ -1163,7 +1459,7 @@ async function lintLocal(cwd, files) {
|
|
|
1163
1459
|
if (!targets.length) return "No instruction files found (looked for CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, \u2026).";
|
|
1164
1460
|
const out = [];
|
|
1165
1461
|
for (const f of targets) {
|
|
1166
|
-
const content = await readFile3(
|
|
1462
|
+
const content = await readFile3(isAbsolute2(f) ? f : join4(cwd, f), "utf8").catch(() => null);
|
|
1167
1463
|
if (content === null) {
|
|
1168
1464
|
out.push(`## ${f}
|
|
1169
1465
|
Could not read the file.`);
|
|
@@ -1177,7 +1473,36 @@ Could not read the file.`);
|
|
|
1177
1473
|
}
|
|
1178
1474
|
var text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...isError ? { isError } : {} });
|
|
1179
1475
|
var server = new Server({ name: "codexguild", version: VERSION }, { capabilities: { tools: { listChanged: true } } });
|
|
1180
|
-
var
|
|
1476
|
+
var WORKFLOWS_TOOL = {
|
|
1477
|
+
name: "codexguild_scan_workflows",
|
|
1478
|
+
description: "Security-review the GitHub Actions workflows in this project that run coding agents (claude-code-action, run-gemini-cli, codex, opencode, aider\u2026): triggers outside contributors control (pull_request_target, issue_comment, issues) combined with secrets, issue/PR text interpolated into prompts or shells (prompt injection), unrestricted agent tools, broad permissions, unpinned agent actions. Runs locally \u2014 nothing is uploaded.",
|
|
1479
|
+
inputSchema: {
|
|
1480
|
+
type: "object",
|
|
1481
|
+
properties: { files: { type: "array", items: { type: "string" }, description: "Workflow files to review instead of .github/workflows/*.yml" } }
|
|
1482
|
+
},
|
|
1483
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
1484
|
+
};
|
|
1485
|
+
async function scanWorkflowsLocal(cwd, files) {
|
|
1486
|
+
const dir = join4(cwd, ".github", "workflows");
|
|
1487
|
+
const targets = files?.length ? files : (await readdir3(dir).catch(() => [])).filter((f) => /\.ya?ml$/.test(f)).map((f) => join4(dir, f));
|
|
1488
|
+
if (!targets.length) return "No workflows found in .github/workflows.";
|
|
1489
|
+
const out = [];
|
|
1490
|
+
for (const f of targets) {
|
|
1491
|
+
const content = await readFile3(isAbsolute2(f) ? f : join4(cwd, f), "utf8").catch(() => null);
|
|
1492
|
+
const name = f.startsWith(cwd + "/") ? f.slice(cwd.length + 1) : f;
|
|
1493
|
+
if (content === null) {
|
|
1494
|
+
out.push(`## ${name}
|
|
1495
|
+
Could not read the file.`);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const r = lintWorkflow(content);
|
|
1499
|
+
if (!r.agents.length && !r.issues.length) continue;
|
|
1500
|
+
out.push(`## ${name} \u2014 ${r.score}/100 (${r.grade})${r.agents.length ? ` \xB7 runs ${r.agents.join(", ")}` : ""}${r.untrustedTriggers.length ? ` \xB7 untrusted triggers: ${r.untrustedTriggers.join(", ")}` : ""}`);
|
|
1501
|
+
out.push(...r.issues.length ? r.issues.map((i) => `- [${i.severity}]${i.line ? ` line ${i.line}` : ""} ${i.title}: ${i.detail}`) : ["- No issues."]);
|
|
1502
|
+
}
|
|
1503
|
+
return out.length ? out.join("\n") : `Reviewed ${targets.length} workflow(s): none runs a coding agent and no injection risks were found.`;
|
|
1504
|
+
}
|
|
1505
|
+
var LOCAL_TOOLS = [SCAN_TOOL, LINT_TOOL, WORKFLOWS_TOOL];
|
|
1181
1506
|
var RETRY_MS = [1e3, 3e3];
|
|
1182
1507
|
var RECONNECT_MS = 3e4;
|
|
1183
1508
|
var reconnectTimer = null;
|
|
@@ -1218,6 +1543,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1218
1543
|
const { name } = req.params;
|
|
1219
1544
|
const args2 = { ...req.params.arguments ?? {} };
|
|
1220
1545
|
try {
|
|
1546
|
+
if (name === WORKFLOWS_TOOL.name) {
|
|
1547
|
+
return text(await scanWorkflowsLocal(process.cwd(), Array.isArray(args2.files) ? args2.files.map(String) : void 0));
|
|
1548
|
+
}
|
|
1221
1549
|
if (name === LINT_TOOL.name) {
|
|
1222
1550
|
return text(await lintLocal(process.cwd(), Array.isArray(args2.files) ? args2.files.map(String) : void 0));
|
|
1223
1551
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexguild/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CodexGuild MCP server (stdio) \u2014 forwards to the remote CodexGuild MCP endpoint and adds local stack detection and an offline security audit of installed agent skills",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|