@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.
@@ -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.15.1 --strict
7
+ entry: npx @yawlabs/ctxlint@0.16.0 --strict
8
8
  language: node
9
9
  always_run: true
10
10
  pass_filenames: false
@@ -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
- - 28 lint rules organized into 8 categories with defined severities
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
- 28 rules organized into 8 categories. Each rule has a unique ID, severity level, trigger condition, and message template.
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-(proj-)?[A-Za-z0-9_-]{20,} # OpenAI (classic or project-scoped) / generic
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)** | 28 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. |
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 isGitTracked = await checkGitTracked(file2.absolutePath, projectRoot);
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 false;
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: "Add YAML frontmatter with a trigger field (always_on, glob, manual, model)"
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.15.1";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "mcpName": "io.github.YawLabs/ctxlint",
5
5
  "description": "Lint your AI agent context files, MCP server configs, and session data against your actual codebase",
6
6
  "bin": {