Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ee43df1
docs: design official plugin compatibility
Esquetta Aug 9, 2026
a23ab89
docs: plan official plugin compatibility
Esquetta Aug 9, 2026
5685ce8
feat: normalize official MCP config layouts
Esquetta Aug 9, 2026
c53dc26
fix: preserve prototype-sensitive MCP server names
Esquetta Aug 9, 2026
8c846a2
feat: validate official MCP package layouts
Esquetta Aug 9, 2026
86faba8
feat: use normalized MCP layouts across workflows
Esquetta Aug 9, 2026
a913cd4
test: complete MCP layout parity coverage
Esquetta Aug 9, 2026
da5f035
feat: validate official plugin components
Esquetta Aug 9, 2026
5608c82
fix: align scaffold and path findings
Esquetta Aug 9, 2026
31e1138
feat: validate plugin lifecycle hooks
Esquetta Aug 9, 2026
6c9996b
fix: harden hook security findings
Esquetta Aug 9, 2026
de7fa41
fix: preserve precise hook audit findings
Esquetta Aug 9, 2026
f2d6288
docs: document official plugin validation
Esquetta Aug 9, 2026
45146fc
chore: prepare v1.58.0 release metadata
Esquetta Aug 9, 2026
27c63b8
Harden runtime plan MCP containment
Esquetta Aug 11, 2026
7be8b89
Redact split runtime secret arguments
Esquetta Aug 11, 2026
b67b5fb
Redact bearer header runtime arguments
Esquetta Aug 11, 2026
c7ff0cf
Harden runtime argument and hook redaction
Esquetta Aug 11, 2026
36dd98f
Cover transparent env wrapper modifiers
Esquetta Aug 11, 2026
f513f16
Finalize v1.58 release metadata
Esquetta Aug 11, 2026
38eec34
Fix v1.58 release notes test and remove internal plans
Esquetta Aug 11, 2026
8c90ab0
Fail closed on malformed MCP manifests
Esquetta Aug 11, 2026
fb5dcd0
Avoid shell for doctor size npm pack
Esquetta Aug 11, 2026
0e5dd7c
Fix cross-platform MCP CI checks
Esquetta Aug 11, 2026
ee4f678
Fail MCP security audit on config escapes
Esquetta Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ All notable changes to `codex-plugin-doctor` are documented here.

This changelog groups the shipped work into product-level release blocks instead of repeating every low-level git diff in isolation.

## [1.58.0] - 2026-08-11

### Added

- added support for current official MCP layouts, including direct server maps, `mcp_servers`, and legacy `mcpServers` wrappers
- added static validation for optional manifest component fields and referenced app and lifecycle-hook definitions

### Changed

- normalized supported MCP layouts before static validation, compatibility planning, and runtime planning consume server definitions

### Security

- kept app and hook validation local, static, and non-executing while applying hook command security checks with relative, redacted evidence
- constrained MCP consumer previews to the canonical package root
- made runtime plans fail closed when security input is invalid or cannot be safely inspected
- redacted credential-bearing runtime arguments from portable plans while keeping approval digests bound to the exact arguments
- hardened lifecycle-hook detection for remote content piped into shells

## [1.57.0] - 2026-08-08

### Added

- added `doctor size --npm` to measure the real npm pack dry-run publish size and warn when local and publish sizes differ by more than 50%
- added repeatable `check --fail-on <rule-id>` so CI can promote selected warning rules to failures

## [1.56.0] - 2026-08-02

### Added

- added `doctor size <path> [--json]` with total size, file count, and the ten largest files
- added package-size findings for files larger than 1 MB, warnings above 10 MB, and failures above 50 MB
- exported the package-size analysis surfaces for programmatic consumers

## [1.55.0] - 2026-07-30

### Added
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
runtime: "true"
policy: codex-publish
Expand Down Expand Up @@ -518,6 +518,7 @@ The validator is tuned against local fixtures and real marketplace-style plugin
- [Real-World Validation Workflow](./docs/guides/real-world-validation.md)
- [Examples](./examples/README.md)
- [Rule Catalog](./docs/rules/catalog.md)
- [Official Plugin Components](./docs/architecture/official-plugin-components.md)

Recent validation waves covered:

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory contains public documentation for users, contributors, and securi

- [Architecture Overview](architecture/overview.md)
- [Validation Engine](architecture/validation-engine.md)
- [Official Plugin Components](architecture/official-plugin-components.md)
- [Suppression Management](architecture/suppression-management.md)
- [Runtime Sandbox and External Corpus](architecture/runtime-sandbox-and-external-corpus.md)
- [MCP 2025-11 Conformance](architecture/mcp-2025-11-conformance.md)
Expand Down
33 changes: 33 additions & 0 deletions docs/architecture/official-plugin-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Official Plugin Components

Codex Plugin Doctor statically validates the supported package configuration surfaces. It reads local package files only: it does not execute hooks or apps, start MCP servers, fetch URLs, authenticate, publish, or change package or client configuration.

## MCP package configuration

Package `.mcp.json` files can use one of these layouts:

1. A direct top-level server map.
2. An `mcp_servers` wrapper containing the server map.
3. The legacy `mcpServers` wrapper.

Use exactly one non-empty layout. A file that combines wrapper keys, or a wrapper with direct server entries, is ambiguous and fails validation rather than being guessed. Invalid JSON and invalid server-map shapes remain failures.

Doctor normalizes accepted package input before validation and compatibility checks. Destination client configs and install previews remain camel-case `mcpServers`; this package-input compatibility does not change client configuration shapes.

## Optional metadata and apps

Optional metadata is checked only when present. This includes author, homepage, repository, license, keywords, interface values, and local asset paths. URL metadata must be syntactically valid HTTP or HTTPS; Doctor does not make network requests. Local component and asset paths must start with `./` and remain inside the package after path and canonical-path resolution.

An `apps` reference must point to an existing, readable JSON file within the package. The internal `.app.json` schema is not published, so Doctor validates only the reference path, regular-file presence, and JSON syntax. A parseable JSON value is accepted; Doctor does not infer fields, load assets, render an app, or infer app behavior.

## Lifecycle hooks

When the manifest does not define `hooks`, Doctor discovers `hooks/hooks.json` only if that default file exists. A manifest hook source takes precedence over that optional default. Hook files and referenced paths are validated statically and must remain inside the package.

Supported events are `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`, `UserPromptSubmit`, `SubagentStop`, `Stop`, `SessionStart`, `SubagentStart`, and `SessionEnd`. Other events fail validation. Prompt and agent handlers are reported as unsupported because this host skips them; `async: true` is also reported because asynchronous hooks are unsupported. Matchers on `Stop` and `UserPromptSubmit` are accepted but reported as ignored.

Command handlers receive static security checks where applicable. No hook process is spawned during validation, compatibility checks, runtime planning, runtime probing, or report generation.

## Reports

Text, Markdown, JSON, and SARIF reports preserve their existing schemas and exit behavior. Finding evidence uses package-relative paths and field names; reports do not retain absolute host paths, command content, secrets, or remote response bodies.
48 changes: 24 additions & 24 deletions docs/guides/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ The Action transfers these boolean inputs through environment-backed shell varia
Use local Registry metadata gating when the repository contains a `server.json` intended for publication:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
registry-metadata: ./server.json
require-registry-readiness: "true"
Expand All @@ -53,9 +53,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
runtime: "true"
policy: codex-publish
Expand All @@ -82,9 +82,9 @@ Every action run also writes `codex-plugin-doctor-action-manifest.json`. The man
Use SARIF when repository security tooling should ingest validation findings.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
sarif: "true"
```
Expand All @@ -96,9 +96,9 @@ The action writes `codex-plugin-doctor.sarif` into `output-dir`. Uploading it to
Use artifact and summary controls when the workflow needs custom retention or wants to disable generated report uploads.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
output-dir: doctor-ci-reports
artifact-name: codex-plugin-doctor-reports
Expand Down Expand Up @@ -134,11 +134,11 @@ The action also exposes these workflow outputs for follow-up steps:
Use review bundle artifacts when a pull request or release workflow should preserve signed runtime approval, runtime policy, attestation, and release evidence handoff files.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
env:
CODEX_PLUGIN_DOCTOR_SIGNING_KEY: ${{ secrets.CODEX_PLUGIN_DOCTOR_SIGNING_KEY }}
with:
version: "1.57.0"
version: "1.58.0"
path: .
review-bundle: "true"
review-bundle-verify: "true"
Expand Down Expand Up @@ -169,19 +169,19 @@ The CLI can produce badge output for release notes, README automation, or a stat
Use a private corpus metrics manifest to measure reviewed precision, recall, and false-positive share in CI. The action writes only the public-safe metrics report into its artifact directory; snapshots, manifest contents, local paths, and review notes are not copied.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
corpus-metrics-manifest: ../private-corpus/metrics.json
```

This writes `corpus-metrics.json`. To compare the result with a retained report and fail the job on regression:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
corpus-metrics-manifest: ../private-corpus/metrics.json
corpus-metrics-baseline: .doctor-baselines/corpus-metrics.json
Expand Down Expand Up @@ -210,9 +210,9 @@ The history file is newline-delimited JSON. Store it as an artifact, cache, or r
The composite action can also append history directly:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
runtime: "true"
history: validation-history.jsonl
Expand All @@ -232,9 +232,9 @@ Use profiles when a consuming workflow needs a named validation policy instead o
The composite action can pass profiles directly:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
profile: publish
```
Expand All @@ -244,9 +244,9 @@ The composite action can pass profiles directly:
Use policy presets when a workflow should apply one of the opinionated release gates without adding a local `.codex-doctor.json`.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
path: .
policy: codex-publish
```
Expand All @@ -258,9 +258,9 @@ Supported policy values are `codex-publish`, `mcp-strict`, and `security`. The C
Use installed-cache mode only in environments where Codex plugins are already available on the runner.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
installed: "true"
filter: github
runtime: "false"
Expand All @@ -271,9 +271,9 @@ Use installed-cache mode only in environments where Codex plugins are already av
Pin both the action ref and npm package version for reproducible CI:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.57.0
- uses: Esquetta/CodexPluginDoctor@v1.58.0
with:
version: "1.57.0"
version: "1.58.0"
```

Use `version: "latest"` only when the consuming repository intentionally wants automatic CLI upgrades.
16 changes: 15 additions & 1 deletion docs/rules/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ codex-plugin-doctor explain plugin.manifest.missing
| `plugin.manifest.name.missing` | fail | Plugin manifest is missing `name`. |
| `plugin.manifest.version.missing` | fail | Plugin manifest is missing `version`. |
| `plugin.manifest.description.missing` | fail | Plugin manifest is missing `description`. |
| `plugin.manifest.invalid_field` | fail | A plugin manifest optional field is invalid. |
| `plugin.manifest.invalid_path` | fail | A plugin manifest path is not a safe package-relative path. |
| `plugin.app.missing_file` | fail | Manifest points to a missing `.app.json` file. |
| `plugin.app.invalid_json` | fail | Referenced `.app.json` file is not valid JSON. |
| `plugin.app.invalid_path` | fail | A plugin app path is not a safe package-relative path. |
| `plugin.hook.missing_file` | fail | Plugin lifecycle hook source file is missing. |
| `plugin.hook.invalid_json` | fail | Plugin lifecycle hook source is not valid JSON. |
| `plugin.hook.invalid_shape` | fail | Plugin lifecycle hook configuration has an invalid shape. |
| `plugin.hook.invalid_path` | fail | Plugin lifecycle hook source is not a safe package-relative path. |
| `plugin.hook.unsupported_event` | fail | Plugin lifecycle hook event is not supported. |
| `plugin.hook.unsupported_handler` | warn | Plugin lifecycle hook uses a handler type the host skips. |
| `plugin.hook.async_unsupported` | warn | Plugin lifecycle hook requests unsupported asynchronous execution. |
| `plugin.hook.matcher_ignored` | warn | Plugin lifecycle hook matcher is ignored for this event. |
| `plugin.heuristic.description.too_long` | warn | Plugin description is likely too verbose. |

## Skill Rules
Expand All @@ -35,7 +48,8 @@ codex-plugin-doctor explain plugin.manifest.missing
| --- | --- | --- |
| `plugin.mcp.path.missing` | fail | Manifest references a missing `.mcp.json` file. |
| `plugin.mcp.invalid_json` | fail | MCP config is not valid JSON. |
| `plugin.mcp.invalid_shape` | fail | MCP config does not contain a valid `mcpServers` object. |
| `plugin.mcp.invalid_shape` | fail | MCP config does not contain one non-empty direct map, `mcp_servers`, or legacy `mcpServers` wrapper. |
| `plugin.mcp.ambiguous_shape` | fail | MCP config mixes multiple supported layout forms. |
| `plugin.mcp.server.invalid` | fail | MCP server entry is not an object. |
| `plugin.mcp.server.transport.missing` | fail | MCP server entry is missing both `command` and `url`. |
| `mcp.server.transport.conflict` | fail | An MCP server defines both command and URL transports. |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-plugin-doctor",
"version": "1.57.0",
"version": "1.58.0",
"description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
"type": "module",
"main": "./dist/index.js",
Expand Down
10 changes: 4 additions & 6 deletions src/compatibility/claude-desktop-install-preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import { normalizeMcpConfig } from "../core/mcp-config-normalizer.js";
import {
getClaudeDesktopConfigPath,
readMcpConfigPath,
Expand Down Expand Up @@ -69,12 +70,9 @@ export async function buildClaudeDesktopInstallPreview(
throw new Error("No MCP config found for install preview.");
}

const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8")) as {
mcpServers?: unknown;
};
const servers = parsed.mcpServers;
const normalizedConfig = normalizeMcpConfig(JSON.parse(await readFile(mcpConfigPath, "utf8")));

if (!isRecord(servers) || Object.keys(servers).length === 0) {
if (!normalizedConfig.ok) {
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
}

Expand All @@ -83,7 +81,7 @@ export async function buildClaudeDesktopInstallPreview(
configPath,
snippet: {
mcpServers: Object.fromEntries(
Object.entries(servers).map(([serverName, serverConfig]) => [
Object.entries(normalizedConfig.servers).map(([serverName, serverConfig]) => [
serverName,
normalizeServerConfig(serverConfig, rootPath)
])
Expand Down
10 changes: 4 additions & 6 deletions src/compatibility/cline-install-preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile } from "node:fs/promises";
import path from "node:path";

import { normalizeMcpConfig } from "../core/mcp-config-normalizer.js";
import {
getClineMcpConfigPath,
readMcpConfigPath,
Expand Down Expand Up @@ -63,12 +64,9 @@ export async function buildClineInstallPreview(
throw new Error("No MCP config found for install preview.");
}

const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8")) as {
mcpServers?: unknown;
};
const servers = parsed.mcpServers;
const normalizedConfig = normalizeMcpConfig(JSON.parse(await readFile(mcpConfigPath, "utf8")));

if (!isRecord(servers) || Object.keys(servers).length === 0) {
if (!normalizedConfig.ok) {
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
}

Expand All @@ -77,7 +75,7 @@ export async function buildClineInstallPreview(
configPath: getClineMcpConfigPath(environment),
snippet: {
mcpServers: Object.fromEntries(
Object.entries(servers).map(([serverName, serverConfig]) => [
Object.entries(normalizedConfig.servers).map(([serverName, serverConfig]) => [
serverName,
normalizeServerConfig(serverConfig, rootPath)
])
Expand Down
Loading