codex-plugin-doctor 1.38.0 → 1.40.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 CHANGED
@@ -330,7 +330,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
330
330
 
331
331
  `check --profile ci|strict|publish` applies named validation policies. `ci` keeps default behavior, `strict` fails on warnings, and `publish` fails on warnings while enabling runtime probing by default.
332
332
 
333
- `check --explain` adds inline rule catalog context to text reports, including why a finding matters, a more detailed fix path, and a compact example.
333
+ `check --explain` adds inline rule catalog context to text reports, including the problem summary, rule category, why a finding matters, a more detailed fix path, a compact example, and the matching `codex-plugin-doctor explain <finding-id>` follow-up command.
334
334
 
335
335
  `check --badge-json` emits Shields endpoint-compatible JSON such as `{"schemaVersion":1,"label":"doctor","message":"PASS","color":"brightgreen"}`. `check --badge-markdown` emits a static shields.io Markdown badge for README or release notes. Badge output is intentionally limited to single package checks, not `check --installed`.
336
336
 
@@ -402,9 +402,9 @@ jobs:
402
402
  runs-on: ubuntu-latest
403
403
  steps:
404
404
  - uses: actions/checkout@v5
405
- - uses: Esquetta/CodexPluginDoctor@v1.38.0
405
+ - uses: Esquetta/CodexPluginDoctor@v1.40.0
406
406
  with:
407
- version: "1.38.0"
407
+ version: "1.40.0"
408
408
  path: .
409
409
  runtime: "true"
410
410
  policy: codex-publish
@@ -1,39 +1,77 @@
1
1
  import { chmod, mkdir, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- const preCommitHook = [
4
- "#!/usr/bin/env sh",
5
- "# Generated by codex-plugin-doctor init git-hooks",
6
- "# Validates the plugin package before every commit.",
7
- "",
8
- "echo \"Codex Plugin Doctor: running pre-commit validation...\"",
9
- "",
10
- "npx --yes codex-plugin-doctor check . --profile ci",
11
- "",
12
- "if [ $? -ne 0 ]; then",
13
- " echo \"\"",
14
- " echo \"Plugin validation failed. Commit blocked.\"",
15
- " echo \"Run 'codex-plugin-doctor check .' to see full diagnostics.\"",
16
- " exit 1",
17
- "fi",
18
- ""
19
- ].join("\n");
20
- const prePushHook = [
21
- "#!/usr/bin/env sh",
22
- "# Generated by codex-plugin-doctor init git-hooks",
23
- "# Validates the plugin package before every push.",
24
- "",
25
- "echo \"Codex Plugin Doctor: running pre-push validation...\"",
26
- "",
27
- "npx --yes codex-plugin-doctor check . --profile ci --runtime",
28
- "",
29
- "if [ $? -ne 0 ]; then",
30
- " echo \"\"",
31
- " echo \"Plugin validation failed. Push blocked.\"",
32
- " echo \"Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.\"",
33
- " exit 1",
34
- "fi",
35
- ""
36
- ].join("\n");
3
+ const isWindows = process.platform === "win32";
4
+ const preCommitHook = isWindows
5
+ ? [
6
+ "@echo off",
7
+ "REM Generated by codex-plugin-doctor init git-hooks",
8
+ "REM Validates the plugin package before every commit.",
9
+ "",
10
+ "echo Codex Plugin Doctor: running pre-commit validation...",
11
+ "",
12
+ "call npx --yes codex-plugin-doctor check . --profile ci",
13
+ "",
14
+ "if %ERRORLEVEL% neq 0 (",
15
+ " echo.",
16
+ " echo Plugin validation failed. Commit blocked.",
17
+ " echo Run 'codex-plugin-doctor check .' to see full diagnostics.",
18
+ " exit /b 1",
19
+ ")",
20
+ ""
21
+ ].join("\r\n")
22
+ : [
23
+ "#!/usr/bin/env sh",
24
+ "# Generated by codex-plugin-doctor init git-hooks",
25
+ "# Validates the plugin package before every commit.",
26
+ "",
27
+ "echo \"Codex Plugin Doctor: running pre-commit validation...\"",
28
+ "",
29
+ "npx --yes codex-plugin-doctor check . --profile ci",
30
+ "",
31
+ "if [ $? -ne 0 ]; then",
32
+ " echo \"\"",
33
+ " echo \"Plugin validation failed. Commit blocked.\"",
34
+ " echo \"Run 'codex-plugin-doctor check .' to see full diagnostics.\"",
35
+ " exit 1",
36
+ "fi",
37
+ ""
38
+ ].join("\n");
39
+ const prePushHook = isWindows
40
+ ? [
41
+ "@echo off",
42
+ "REM Generated by codex-plugin-doctor init git-hooks",
43
+ "REM Validates the plugin package before every push.",
44
+ "",
45
+ "echo Codex Plugin Doctor: running pre-push validation...",
46
+ "",
47
+ "call npx --yes codex-plugin-doctor check . --profile ci --runtime",
48
+ "",
49
+ "if %ERRORLEVEL% neq 0 (",
50
+ " echo.",
51
+ " echo Plugin validation failed. Push blocked.",
52
+ " echo Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.",
53
+ " exit /b 1",
54
+ ")",
55
+ ""
56
+ ].join("\r\n")
57
+ : [
58
+ "#!/usr/bin/env sh",
59
+ "# Generated by codex-plugin-doctor init git-hooks",
60
+ "# Validates the plugin package before every push.",
61
+ "",
62
+ "echo \"Codex Plugin Doctor: running pre-push validation...\"",
63
+ "",
64
+ "npx --yes codex-plugin-doctor check . --profile ci --runtime",
65
+ "",
66
+ "if [ $? -ne 0 ]; then",
67
+ " echo \"\"",
68
+ " echo \"Plugin validation failed. Push blocked.\"",
69
+ " echo \"Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.\"",
70
+ " exit 1",
71
+ "fi",
72
+ ""
73
+ ].join("\n");
74
+ const hookExtension = isWindows ? ".bat" : "";
37
75
  async function fileExists(filePath) {
38
76
  try {
39
77
  await stat(filePath);
@@ -57,7 +95,7 @@ async function writeHook(hookPath, content, force) {
57
95
  export async function removeGitHooks(targetPath) {
58
96
  const rootPath = path.resolve(targetPath);
59
97
  const hooksDir = path.join(rootPath, ".git", "hooks");
60
- const hookNames = ["pre-commit", "pre-push"];
98
+ const hookNames = Array.from(new Set([`pre-commit${hookExtension}`, `pre-push${hookExtension}`, "pre-commit", "pre-push"]));
61
99
  const removed = [];
62
100
  const skipped = [];
63
101
  for (const name of hookNames) {
@@ -82,8 +120,8 @@ export async function initGitHooks(targetPath, options = {}) {
82
120
  await mkdir(hooksDir, { recursive: true });
83
121
  const hookPaths = [];
84
122
  const preExisting = [];
85
- const preCommitPath = path.join(hooksDir, "pre-commit");
86
- const prePushPath = path.join(hooksDir, "pre-push");
123
+ const preCommitPath = path.join(hooksDir, `pre-commit${hookExtension}`);
124
+ const prePushPath = path.join(hooksDir, `pre-push${hookExtension}`);
87
125
  const wasPreCommitExisting = await writeHook(preCommitPath, preCommitHook, force);
88
126
  hookPaths.push(preCommitPath);
89
127
  if (wasPreCommitExisting) {
@@ -76,9 +76,12 @@ export function renderTextReport(result, options = {}) {
76
76
  if (explain) {
77
77
  const rule = findRuleDefinition(finding.id);
78
78
  if (rule) {
79
+ lines.push(` Problem: ${rule.summary}`);
80
+ lines.push(` Rule: ${rule.category}, ${rule.defaultSeverity}`);
79
81
  lines.push(` Why: ${rule.why}`);
80
82
  lines.push(` Fix detail: ${rule.fix}`);
81
83
  lines.push(` Example: ${rule.example}`);
84
+ lines.push(` Learn more: codex-plugin-doctor explain ${finding.id}`);
82
85
  }
83
86
  }
84
87
  }
@@ -260,6 +260,15 @@ export const ruleCatalog = [
260
260
  fix: "Use `127.0.0.1` or `localhost` in server URLs and transport configuration unless external access is explicitly required.",
261
261
  example: '{ "url": "http://127.0.0.1:3000/mcp" }'
262
262
  },
263
+ {
264
+ id: "plugin.skill.external_http_reference",
265
+ category: "skill",
266
+ defaultSeverity: "warn",
267
+ summary: "A packaged skill text references an external HTTP/HTTPS URL.",
268
+ why: "External URLs in skill instructions can lead to link rot, phishing risk, or unauthorized telemetry when an agent follows them.",
269
+ fix: "Replace the external URL with a local reference, or document the link only in the plugin README where reviewers can see it.",
270
+ example: "Replace `https://example.com/docs` with a local `docs/setup.md` reference."
271
+ },
263
272
  {
264
273
  id: "plugin.security.prompt_injection_text",
265
274
  category: "security",
package/dist/run-cli.js CHANGED
@@ -78,7 +78,7 @@ const defaultIo = {
78
78
  }
79
79
  };
80
80
  function printUsage(io) {
81
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
81
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
82
82
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
83
83
  }
84
84
  const suppressUsageText = [
@@ -2254,6 +2254,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2254
2254
  const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
2255
2255
  ? null
2256
2256
  : normalizedFlags[runtimeApprovalDigestIndex + 1];
2257
+ const changedSinceIndex = normalizedFlags.indexOf("--changed-since");
2258
+ const changedSinceRef = changedSinceIndex === -1 ? null : normalizedFlags[changedSinceIndex + 1];
2257
2259
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
2258
2260
  io.writeStderr("Missing path after --output.");
2259
2261
  return 2;
@@ -2282,6 +2284,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
2282
2284
  io.writeStderr("Missing path after --history.");
2283
2285
  return 2;
2284
2286
  }
2287
+ if (changedSinceIndex !== -1 && (!changedSinceRef || changedSinceRef.startsWith("--"))) {
2288
+ io.writeStderr("Missing ref after --changed-since.");
2289
+ return 2;
2290
+ }
2291
+ if (changedSinceIndex !== -1 && checkInstalled) {
2292
+ io.writeStderr("--changed-since requires a single package target.");
2293
+ return 2;
2294
+ }
2285
2295
  if (runtimeApprovalDigestIndex !== -1 &&
2286
2296
  (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
2287
2297
  io.writeStderr("Missing digest after --runtime-approval-digest.");
@@ -2384,6 +2394,59 @@ export async function runCli(args, io = defaultIo, options = {}) {
2384
2394
  ? createLiveStatusRenderer(io, getSpinner(outputPolicy.style === "ascii" ? "ascii" : "doctor"))
2385
2395
  : null;
2386
2396
  renderer?.start("Validating package");
2397
+ if (changedSinceRef) {
2398
+ const { execFile } = await import("node:child_process");
2399
+ try {
2400
+ const gitOutput = await new Promise((resolve, reject) => {
2401
+ execFile("git", ["diff", "--name-only", `${changedSinceRef}...HEAD`], { cwd: path.resolve(targetPath) }, (error, stdout, stderr) => {
2402
+ if (error) {
2403
+ reject(new Error(stderr.trim() || error.message));
2404
+ return;
2405
+ }
2406
+ resolve(stdout);
2407
+ });
2408
+ });
2409
+ const changedFiles = gitOutput.split("\n").filter(Boolean);
2410
+ if (changedFiles.length === 0) {
2411
+ io.writeStdout("No changed files since the given ref.");
2412
+ return 0;
2413
+ }
2414
+ const changedDirs = new Set();
2415
+ for (const file of changedFiles) {
2416
+ const dir = path.dirname(file);
2417
+ changedDirs.add(dir);
2418
+ }
2419
+ let allPassed = true;
2420
+ let validatedCount = 0;
2421
+ for (const dir of changedDirs) {
2422
+ const pluginRoot = path.resolve(targetPath, dir);
2423
+ const manifestPath = path.join(pluginRoot, ".codex-plugin", "plugin.json");
2424
+ try {
2425
+ await (await import("node:fs/promises")).stat(manifestPath);
2426
+ }
2427
+ catch {
2428
+ continue;
2429
+ }
2430
+ const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
2431
+ runtime: effectiveRuntimeProbeEnabled
2432
+ }), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
2433
+ io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
2434
+ validatedCount += 1;
2435
+ if (pluginResult.status !== "pass") {
2436
+ allPassed = false;
2437
+ for (const finding of pluginResult.findings) {
2438
+ io.writeStdout(` ${finding.severity === "fail" ? "FAIL" : "WARN"} ${finding.id}: ${finding.message}`);
2439
+ }
2440
+ }
2441
+ }
2442
+ io.writeStdout(`\n\nValidated ${validatedCount} changed plugins.`);
2443
+ return allPassed ? 0 : 1;
2444
+ }
2445
+ catch (error) {
2446
+ io.writeStderr(`--changed-since requires a git repository. ${error.message}`);
2447
+ return 2;
2448
+ }
2449
+ }
2387
2450
  const result = applyDoctorConfig(await runCheckImpl(targetPath, {
2388
2451
  runtime: effectiveRuntimeProbeEnabled,
2389
2452
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
@@ -254,6 +254,23 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
254
254
  }
255
255
  return findings;
256
256
  }
257
+ const externalUrlPattern = /https?:\/\/[^\s`"'<>)]+/gi;
258
+ async function auditSkillExternalReferences(rootPath) {
259
+ const findings = [];
260
+ for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
261
+ const content = await readFile(filePath, "utf8");
262
+ const matches = content.match(externalUrlPattern);
263
+ if (!matches) {
264
+ continue;
265
+ }
266
+ const uniqueUrls = [...new Set(matches)];
267
+ const relativeFilePath = path.relative(rootPath, filePath).replace(/\\/g, "/");
268
+ for (const url of uniqueUrls.slice(0, 5)) {
269
+ findings.push(buildFinding("warn", "plugin.skill.external_http_reference", `The packaged text file \`${relativeFilePath}\` references external URL \`${url}\`.`, "External URLs in skill instructions can lead to link rot, phishing risk, or unauthorized telemetry when an agent follows them.", "Replace the external URL with a local reference, or document the link only in the plugin README where reviewers can see it.", { filePath: relativeFilePath, url }));
270
+ }
271
+ }
272
+ return findings;
273
+ }
257
274
  async function collectPromptPoisoningScanFiles(rootPath, currentPath = rootPath) {
258
275
  const entries = await readdir(currentPath, { withFileTypes: true });
259
276
  const filePaths = [];
@@ -404,7 +421,8 @@ export async function buildSecurityAudit(targetPath) {
404
421
  ...validationSecurityFindings,
405
422
  ...(await auditMcpCommandSurface(discoveredPackage)),
406
423
  ...(await auditChildProcessSourceSurface(discoveredPackage.rootPath)),
407
- ...(await auditPromptPoisoningSurface(discoveredPackage.rootPath))
424
+ ...(await auditPromptPoisoningSurface(discoveredPackage.rootPath)),
425
+ ...(await auditSkillExternalReferences(discoveredPackage.rootPath))
408
426
  ];
409
427
  return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
410
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.38.0",
3
+ "version": "1.40.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",