@yawlabs/ctxlint 0.15.1 → 0.16.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/.pre-commit-hooks.yaml +1 -1
- package/MCP_CONFIG_LINT_SPEC.md +7 -4
- package/README.md +1 -1
- package/dist/index.js +23 -7
- package/mcp-config-lint-rules.json +13 -0
- package/package.json +1 -1
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
|
|
5
5
|
# of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
|
|
6
6
|
# this in sync with package.json on each bump.
|
|
7
|
-
entry: npx @yawlabs/ctxlint@0.
|
|
7
|
+
entry: npx @yawlabs/ctxlint@0.16.0 --strict
|
|
8
8
|
language: node
|
|
9
9
|
always_run: true
|
|
10
10
|
pass_filenames: false
|
package/MCP_CONFIG_LINT_SPEC.md
CHANGED
|
@@ -16,7 +16,7 @@ This specification defines a standard set of lint rules for validating MCP serve
|
|
|
16
16
|
|
|
17
17
|
The specification includes:
|
|
18
18
|
- A complete reference of MCP config file locations, formats, and client-specific behaviors
|
|
19
|
-
-
|
|
19
|
+
- 29 lint rules organized into 8 categories with defined severities
|
|
20
20
|
- A machine-readable rule catalog ([`mcp-config-lint-rules.json`](./mcp-config-lint-rules.json))
|
|
21
21
|
- Auto-fix definitions for rules that support automated correction
|
|
22
22
|
|
|
@@ -199,7 +199,7 @@ Without this wrapper, the subprocess fails to spawn. This is the most common Win
|
|
|
199
199
|
|
|
200
200
|
## 2. Lint Rules
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
29 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
|
|
203
203
|
|
|
204
204
|
Severity levels:
|
|
205
205
|
- **error** — the config is broken or has a security issue. Should fail CI.
|
|
@@ -226,19 +226,22 @@ Validates that the config file is well-formed JSON with the correct structure fo
|
|
|
226
226
|
|
|
227
227
|
### 2.2 mcp-security — hardcoded secrets
|
|
228
228
|
|
|
229
|
-
Detects secrets committed to version control in MCP config files. The three secret rules (`hardcoded-bearer`, `hardcoded-api-key`, `secret-in-url`) only flag issues in git-tracked files — an untracked config leaks nothing to teammates. `mcp-security/http-no-tls` is a transport concern, independent of version control, and fires regardless of git tracking.
|
|
229
|
+
Detects secrets committed to version control in MCP config files. The three secret rules (`hardcoded-bearer`, `hardcoded-api-key`, `secret-in-url`) only flag issues in git-tracked files — an untracked config leaks nothing to teammates. `mcp-security/http-no-tls` is a transport concern, independent of version control, and fires regardless of git tracking. When the tracked status cannot be determined at all (git unavailable or failing, as opposed to a determined "untracked"), the linter says so via `mcp-security/secret-scan-skipped` instead of silently passing a possibly-tracked file.
|
|
230
230
|
|
|
231
231
|
| Rule ID | Severity | Trigger | Message |
|
|
232
232
|
|---|---|---|---|
|
|
233
233
|
| `mcp-security/hardcoded-bearer` | error | `Authorization` header contains a literal Bearer token (not an env var reference) in a git-tracked file | `Server "{name}" has a hardcoded Bearer token in a git-tracked file` |
|
|
234
234
|
| `mcp-security/hardcoded-api-key` | error | Header or env value matches known API key patterns (or the high-entropy heuristic below) in a git-tracked file | `Server "{name}" has a hardcoded API key in a git-tracked file` |
|
|
235
235
|
| `mcp-security/secret-in-url` | error | URL contains query params that look like secrets (`?key=`, `?token=`, `?api_key=`) in a git-tracked file | `Server "{name}" has a secret in the URL query string` |
|
|
236
|
+
| `mcp-security/secret-scan-skipped` | info | Git-tracked status could not be determined (git unavailable/failing — not merely untracked), so the three git-gated secret rules were skipped | `Could not determine git-tracked status of {file}; hardcoded-secret rules were skipped` |
|
|
236
237
|
| `mcp-security/http-no-tls` | warning | URL uses `http://` for a non-loopback target (loopback = `localhost`, `[::1]`, `127.0.0.0/8`) | `Server "{name}" uses HTTP without TLS` |
|
|
237
238
|
|
|
238
239
|
**Known API key patterns:**
|
|
239
240
|
```
|
|
240
241
|
sk-ant-[A-Za-z0-9_-]{20,} # Anthropic
|
|
241
|
-
sk-
|
|
242
|
+
sk-proj-[A-Za-z0-9_-]{20,} # OpenAI project-scoped
|
|
243
|
+
sk-[a-zA-Z0-9]{20,} # OpenAI classic / generic (alphanumeric-only:
|
|
244
|
+
# [-_] would swallow kebab-case identifiers)
|
|
242
245
|
ghp_[a-zA-Z0-9]{36} # GitHub personal access token
|
|
243
246
|
ghu_[a-zA-Z0-9]{36} # GitHub user token
|
|
244
247
|
github_pat_[a-zA-Z0-9_]{80,} # GitHub fine-grained PAT
|
package/README.md
CHANGED
|
@@ -514,7 +514,7 @@ ctxlint is the reference implementation of four open specifications for linting
|
|
|
514
514
|
| Spec | What it covers |
|
|
515
515
|
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
516
516
|
| **[AI Context File Linting Spec](./CONTEXT_LINT_SPEC.md)** | 39 rules for validating context files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) across 16 clients. Covers file formats, frontmatter schemas, path/command validation, staleness, token budgets, redundancy, and contradictions. |
|
|
517
|
-
| **[MCP Config Linting Spec](./MCP_CONFIG_LINT_SPEC.md)** |
|
|
517
|
+
| **[MCP Config Linting Spec](./MCP_CONFIG_LINT_SPEC.md)** | 29 rules for validating MCP server configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, etc.) across 8 clients. Covers schema validation, hardcoded secrets, env var syntax, deprecated transports, and cross-file consistency. |
|
|
518
518
|
| **[Agent Session Linting Spec](./AGENT_SESSION_LINT_SPEC.md)** | 8 rules for auditing agent session data (history, memory) across 8 agents. Covers cross-project secret consistency, config drift, stale memory, and loop detection. |
|
|
519
519
|
| **[Agent Skill Linting Spec](./AGENT_SKILL_LINT_SPEC.md)** | 5 rules for auditing Claude Code skill (`SKILL.md`) and agent (`.md`) definitions under `~/.claude`. Covers frontmatter presence, broken refs, trigger-phrase collisions, orphaned skills, and dead tool restrictions. (v1, experimental) |
|
|
520
520
|
|
package/dist/index.js
CHANGED
|
@@ -41087,7 +41087,7 @@ async function parseMcpConfig(file2, projectRoot, scope) {
|
|
|
41087
41087
|
const content = readFileContent(file2.absolutePath);
|
|
41088
41088
|
const client = detectClient(file2.relativePath);
|
|
41089
41089
|
const expectedRootKey = client === "vscode" ? "servers" : "mcpServers";
|
|
41090
|
-
const
|
|
41090
|
+
const gitTracked = await checkGitTracked(file2.absolutePath, projectRoot);
|
|
41091
41091
|
const result = {
|
|
41092
41092
|
filePath: file2.absolutePath,
|
|
41093
41093
|
relativePath: file2.relativePath,
|
|
@@ -41098,7 +41098,8 @@ async function parseMcpConfig(file2, projectRoot, scope) {
|
|
|
41098
41098
|
servers: [],
|
|
41099
41099
|
parseErrors: [],
|
|
41100
41100
|
content,
|
|
41101
|
-
isGitTracked
|
|
41101
|
+
isGitTracked: gitTracked === "tracked",
|
|
41102
|
+
...gitTracked === "unknown" ? { gitTrackedUnknown: true } : {}
|
|
41102
41103
|
};
|
|
41103
41104
|
let parsed;
|
|
41104
41105
|
try {
|
|
@@ -41283,13 +41284,18 @@ function decodeJsonKey(raw) {
|
|
|
41283
41284
|
return raw;
|
|
41284
41285
|
}
|
|
41285
41286
|
}
|
|
41287
|
+
function classifyGitTrackedError(message) {
|
|
41288
|
+
if (/did not match any file/i.test(message)) return "untracked";
|
|
41289
|
+
if (/not a git repository/i.test(message)) return "untracked";
|
|
41290
|
+
return "unknown";
|
|
41291
|
+
}
|
|
41286
41292
|
async function checkGitTracked(filePath, projectRoot) {
|
|
41287
41293
|
try {
|
|
41288
41294
|
const git = getGit(projectRoot);
|
|
41289
41295
|
const result = await git.raw(["ls-files", "--error-unmatch", filePath]);
|
|
41290
|
-
return result.trim().length > 0;
|
|
41291
|
-
} catch {
|
|
41292
|
-
return
|
|
41296
|
+
return result.trim().length > 0 ? "tracked" : "untracked";
|
|
41297
|
+
} catch (err) {
|
|
41298
|
+
return classifyGitTrackedError(err instanceof Error ? err.message : String(err));
|
|
41293
41299
|
}
|
|
41294
41300
|
}
|
|
41295
41301
|
var init_mcp_parser = __esm({
|
|
@@ -44110,7 +44116,7 @@ function validateWindsurfRule(file2) {
|
|
|
44110
44116
|
ruleId: "frontmatter/missing",
|
|
44111
44117
|
line: 1,
|
|
44112
44118
|
message: "Windsurf rule file has no frontmatter",
|
|
44113
|
-
suggestion:
|
|
44119
|
+
suggestion: `Add YAML frontmatter with a trigger field (${VALID_WINDSURF_TRIGGERS.join(", ")})`
|
|
44114
44120
|
});
|
|
44115
44121
|
return issues;
|
|
44116
44122
|
}
|
|
@@ -44301,6 +44307,16 @@ async function checkMcpSecurity(config2, _projectRoot) {
|
|
|
44301
44307
|
const issues = [];
|
|
44302
44308
|
if (config2.parseErrors.length > 0) return issues;
|
|
44303
44309
|
const checkSecrets = config2.isGitTracked;
|
|
44310
|
+
if (!checkSecrets && config2.gitTrackedUnknown) {
|
|
44311
|
+
issues.push({
|
|
44312
|
+
severity: "info",
|
|
44313
|
+
check: "mcp-security",
|
|
44314
|
+
ruleId: "mcp-security/secret-scan-skipped",
|
|
44315
|
+
line: 1,
|
|
44316
|
+
message: `Could not determine git-tracked status of ${config2.relativePath}; hardcoded-secret rules were skipped`,
|
|
44317
|
+
suggestion: "Verify git is available and the file is not tracked, or re-run inside the repository"
|
|
44318
|
+
});
|
|
44319
|
+
}
|
|
44304
44320
|
for (const server2 of config2.servers) {
|
|
44305
44321
|
if (checkSecrets && server2.headers) {
|
|
44306
44322
|
for (const [headerName, headerValue] of Object.entries(server2.headers)) {
|
|
@@ -46885,7 +46901,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
46885
46901
|
import { resolve as resolve13, dirname as dirname6 } from "node:path";
|
|
46886
46902
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
46887
46903
|
function loadVersion() {
|
|
46888
|
-
if (true) return "0.
|
|
46904
|
+
if (true) return "0.16.0";
|
|
46889
46905
|
const __dir = dirname6(fileURLToPath2(import.meta.url));
|
|
46890
46906
|
const pkgPath = resolve13(__dir, "../package.json");
|
|
46891
46907
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
@@ -206,6 +206,19 @@
|
|
|
206
206
|
],
|
|
207
207
|
"stability": "stable"
|
|
208
208
|
},
|
|
209
|
+
{
|
|
210
|
+
"id": "mcp-security/secret-scan-skipped",
|
|
211
|
+
"category": "mcp-security",
|
|
212
|
+
"severity": "info",
|
|
213
|
+
"description": "Git-tracked status of the config file could not be determined, so the git-gated secret rules were skipped without an answer.",
|
|
214
|
+
"trigger": "git ls-files fails for a reason other than \"untracked\" or \"not a repository\" (git unavailable, permissions, timeout). A determined \"untracked\" does not fire this rule.",
|
|
215
|
+
"message": "Could not determine git-tracked status of {file}; hardcoded-secret rules were skipped",
|
|
216
|
+
"fixable": false,
|
|
217
|
+
"clients": [
|
|
218
|
+
"all"
|
|
219
|
+
],
|
|
220
|
+
"stability": "experimental"
|
|
221
|
+
},
|
|
209
222
|
{
|
|
210
223
|
"id": "mcp-security/http-no-tls",
|
|
211
224
|
"category": "mcp-security",
|
package/package.json
CHANGED