Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@
"mcp",
"infrastructure"
],
"mcpServers": "./mcp.json",
"hooks": "./hooks/claude-code.json"
"commands": []
}
16 changes: 14 additions & 2 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,28 @@
"homepage": "https://nexlayer.com",
"repository": "https://github.com/Nexlayer/nexlayer-plugin",
"license": "MIT",
"keywords": ["deploy", "cloud", "hosting", "containers", "ai", "mcp", "infrastructure"],
"keywords": [
"deploy",
"cloud",
"hosting",
"containers",
"ai",
"mcp",
"infrastructure"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "Nexlayer",
"shortDescription": "Ship containerized apps to a live URL",
"longDescription": "Nexlayer turns a repo into a running production app: generate the Dockerfile, build and push the image, write and validate nexlayer.yaml, deploy, and hand back a live URL. Includes debugging for what you shipped — logs, events, shells, and live database queries.",
"developerName": "Nexlayer",
"category": "Developer Tools",
"capabilities": ["Read", "Write"],
"capabilities": [
"Read",
"Write"
],
"websiteURL": "https://nexlayer.com",
"privacyPolicyURL": "https://nexlayer.com/legal/privacy",
"termsOfServiceURL": "https://nexlayer.com/legal/terms",
Expand Down
16 changes: 8 additions & 8 deletions .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"name": "nexlayer",
"displayName": "Nexlayer",
"version": "1.0.0",
"description": "Deploy any containerized application to Nexlayer and get a live URL \u2014 without leaving your editor.",
"description": "Deploy any containerized application to Nexlayer and get a live URL — without leaving your editor.",
"author": {
"name": "Nexlayer",
"email": "support@nexlayer.com"
},
"homepage": "https://nexlayer.com",
"repository": "https://github.com/Nexlayer/nexlayer-plugin",
"license": "MIT",
"logo": "assets/logo.svg",
"logo": "./assets/logo.svg",
"keywords": [
"deploy",
"cloud",
Expand All @@ -20,10 +20,10 @@
"mcp",
"infrastructure"
],
"skills": "skills",
"commands": "commands",
"agents": "agents",
"rules": "rules",
"mcpServers": "mcp.json",
"hooks": "hooks/hooks.json"
"skills": "./skills/",
"commands": "./commands/",
"agents": "./agents/",
"rules": "./rules/",
"mcpServers": "./mcp.json",
"hooks": "./hooks/cursor.json"
}
13 changes: 10 additions & 3 deletions .devin-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
"homepage": "https://nexlayer.com",
"repository": "https://github.com/Nexlayer/nexlayer-plugin",
"license": "MIT",
"keywords": ["deploy", "cloud", "hosting", "containers", "ai", "mcp", "infrastructure"],
"keywords": [
"deploy",
"cloud",
"hosting",
"containers",
"ai",
"mcp",
"infrastructure"
],
"skills": "./skills/",
"mcpServers": "./mcp.json",
"agentSubagents": "./agents/"
"mcpServers": "./mcp.json"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ __pycache__/
*.pfx
credentials.json
service-account*.json

# Local project memory — maintainer notes, not published
CLAUDE.md
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ First release.
- Codex support: `.codex-plugin/plugin.json` with listing metadata and `.agents/plugins/marketplace.json`, so `codex plugin marketplace add Nexlayer/nexlayer-plugin` works
- `.devin-plugin/plugin.json` — Devin CLI shipped plugins; it also honors the root Agent Plugins manifest
- `com.github.copilot/agents/nexlayer-deploy.agent.md` so VS Code and Copilot get the subagent, generated by `scripts/gen-host-components.py` and drift-checked
- `hooks/nexlayer-yaml-check.py`: advisory `nexlayer.yaml` check on file edit, covering the five hard constraints the server-side validator returns VALID for, plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. Wired for Cursor (`hooks/hooks.json`, `afterFileEdit`) and Claude Code (`hooks/claude-code.json`, `PostToolUse`)
- `hooks/nexlayer-yaml-check.py`: advisory `nexlayer.yaml` check on file edit, covering the five hard constraints the server-side validator returns VALID for, plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. `hooks/hooks.json` is the nested Claude Code + Codex schema (`PostToolUse`, matcher covers `Write|Edit|MultiEdit|NotebookEdit|apply_patch`); `hooks/cursor.json` is Cursor's flat schema. Finds the edited file in Claude Code, Cursor, Codex `apply_patch`, and Copilot payloads
- `.mcp.json` alongside `mcp.json` — Claude Code discovers MCP servers only from the dot-prefixed name; the manifest `mcpServers` key is ignored. Verified by real install
- `.claude-plugin/plugin.json` carries no `hooks` or `mcpServers` pointer (Claude Code reads the default locations regardless) and sets `"commands": []`, because `commands/` is deprecated there and duplicated the skills' own slash names
- Every path-valued manifest key is `./`-prefixed; `.devin-plugin/plugin.json` drops the undocumented `agentSubagents` key
- `validate.py` encodes every host-loading rule found by real installs — hook file schemas and wiring per host, `.mcp.json`/`mcp.json` agreement, Agent Plugins transport values, `./` paths, Devin's documented key set, Agent Skills frontmatter shape — each confirmed to fire by breaking it
- `assets/logo.png` (512×512) for hosts whose listings want a raster logo
- `scripts/sync-from-mcp.sh` — resync or drift-check skills and the tool list against the MCP repo
- `scripts/validate.py` — spec schemas, skill frontmatter, links, MCP tool names, manifest agreement, hook scripts
- `patches/` layer: `sync-from-mcp.sh` reapplies patches with `git apply` after every sync and exits 3 if one goes stale, so a local fix can neither rot nor be silently reverted
- `0001` — `references/MCP-SETUP.md`: dashboard is `app.nexlayer.com` (there is no `app.nexlayer.io`), transport is `http` not the deprecated `sse`, Cursor config path is `~/.cursor/mcp.json`, and Claude Code leads with `npx @nexlayer/mcp-install` — all matching nexlayer.com/docs/mcp. Upstream: claudecode-mcp-go#45
- `0002` — scrubbed a real-shaped account identifier from the registry example and removed a dead internal-tool link
- `0004` — `allowed-tools` frontmatter split into one `Tool(pattern)` per token, per the Agent Skills spec
- `0003` — scrubbed the remaining internal tool name from `SKILL.md` frontmatter and both antipattern references, and replaced named third-party platforms in `SKILL.md`'s decision tree and reference table with neutral wording
- Pre-publication security review (`docs/SECURITY-REVIEW.md`): no credentials in the tree or in git history, registry rejects anonymous access, workflow token scoped read-only with actions pinned to SHAs, `SECURITY.md` added
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Do not hand-edit anything under `skills/` here. Edit it upstream, then resync. T
scripts/sync-from-mcp.sh # pull skills + tool list from the MCP repo
scripts/sync-from-mcp.sh --check # report drift without changing anything
scripts/gen-host-components.py # regenerate host-namespace mirrors
python3 scripts/validate.py # schemas, frontmatter, links, tool names, manifests, hooks
python3 scripts/validate.py # schemas, frontmatter, links, tool names, host wiring, hook schemas
claude plugin validate . --strict # Anthropic's own gate, run by their review pipeline
```

Expand All @@ -92,14 +92,14 @@ The bundle is also tested against the production MCP server, not just the source
```
plugin.json Agent Plugins 1.0 manifest (portable core)
mcp.json Portable Agent Plugins MCP server config
.mcp.json Codex MCP server config
.mcp.json Same servers, dot-prefixed — Claude Code, Codex, Devin, Copilot CLI read this name
skills/ ship-it-nexlayer, debug-nexlayer (verbatim from the MCP repo)
commands/ agents/ rules/ Client extensions — thin wrappers over the skills
.cursor-plugin/plugin.json Cursor manifest
.claude-plugin/ Claude Code manifest and marketplace entry
.codex-plugin/plugin.json Codex manifest, MCP pointer, and listing metadata
.agents/plugins/ Codex marketplace entry
hooks/ nexlayer.yaml checker + per-host hook config
hooks/ nexlayer.yaml checker; hooks.json (Claude Code + Codex), cursor.json (Cursor)
com.github.copilot/ Copilot namespace (generated mirror of agents/)
patches/ Documented deviations from canon, reapplied on every sync
scripts/ sync-from-mcp.sh, validate.py, generated tool list
Expand Down
38 changes: 28 additions & 10 deletions docs/PLATFORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,50 @@ A `nexlayer-cursor` / `nexlayer-codex` / `nexlayer-claude-code` split would fork
|--------|-------------------|:------:|:---:|:--------:|:--------:|:-----:|:-----:|---------|
| Claude Code | `.claude-plugin/plugin.json` | ✅ | ✅ | ✅ | ✅ | — | ✅ | `/plugin marketplace add Nexlayer/nexlayer-plugin` |
| Cursor | `.cursor-plugin/plugin.json` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Customize → Nexlayer → Install |
| Codex | `.codex-plugin/plugin.json` | ✅ | ✅ | — | — | — | ➖ | `codex plugin marketplace add Nexlayer/nexlayer-plugin` |
| Codex | `.codex-plugin/plugin.json` | ✅ | ✅ | — | — | — | ✅ | `codex plugin marketplace add Nexlayer/nexlayer-plugin` |
| VS Code / Copilot | `plugin.json` + `com.github.copilot/` | ✅ | ✅ | — | ✅ | — | ➖ | Chat: Install Plugin From Source → repo URL |
| Devin CLI | `.devin-plugin/plugin.json` | ✅ | ✅ | — | ✅ | ✅ | ➖ | `devin plugins install Nexlayer/nexlayer-plugin` |
| Devin CLI | `.devin-plugin/plugin.json` | ✅ | ✅ | — | ✅ | ✅ | — | `devin plugins install Nexlayer/nexlayer-plugin` |
| Grok | `.claude-plugin/plugin.json` | ✅ | ✅ | ✅ | ✅ | — | ✅ | Add repo as a marketplace source, then trust |
| Windsurf | none — MCP only | — | ✅ | — | — | — | — | `mcp.json` snippet from `references/MCP-SETUP.md` |
| Cline / Roo / Kilo | none — MCP only | — | ✅ | — | — | — | — | Same snippet, per-client config path |
| Anything else with MCP | none | — | ✅ | — | — | — | — | Point it at `https://mcp.nexlayer.ai/api/mcp` |

✅ shipped · ➖ host supports it, hook schema not verified yet · — host has no such concept
✅ shipped · ➖ host supports it, plugin-root variable for the hook command not documented, so left off · — host has no such concept or reads a location this repo does not ship

**Hooks.** `hooks/nexlayer-yaml-check.py` runs on every file edit and checks `nexlayer.yaml` for the things the server-side validator lets through — untagged image, empty `servicePorts`, no pod with `path`, invalid pod name, unknown fields — plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. It is advisory: findings go to stdout, exit code is always 0, and it stays silent on a clean file or an unrelated edit.
Only Claude Code has been exercised by a live install (see the loading-rules section below and `docs/VALIDATION.md`). Codex was exercised by the Codex plugin validator and an isolated `CODEX_HOME` install. Cursor, VS Code, Devin, and Grok are conformance-by-documentation.

Cursor and Claude Code both read `hooks/hooks.json` by default but with **different schemas**, so Cursor keeps the default name (`hooks/hooks.json`, `"version": 1` plus `afterFileEdit`) and Claude Code gets an explicitly-named file (`hooks/claude-code.json` with `PostToolUse` and `${CLAUDE_PLUGIN_ROOT}`) that `.claude-plugin/plugin.json` points at. Marketplace scanners look for the default name, so a non-default filename means the hook is never detected.
**Hooks.** `hooks/nexlayer-yaml-check.py` runs after a file edit and checks `nexlayer.yaml` for the things the server-side validator lets through — untagged image, empty `servicePorts`, no pod with `path`, invalid pod name, unknown fields — plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. It is advisory: findings go to stdout, exit code is always 0, and it stays silent on a clean file or an unrelated edit. It walks every string in whatever JSON the host sends, so it finds the path in Claude Code's `tool_input.file_path`, Cursor's `file_path`, Codex's `apply_patch` patch text (resolved against `cwd`), and Copilot's `toolArgs`.

Two Claude Code loading rules were found by installing the plugin and reading `claude plugin details`, not from a spec — both fail silently and both pass every schema check:
Three hosts default to the same filename with two schemas, so the layout is:

| File | Schema | Read by | How |
|------|--------|---------|-----|
| `hooks/hooks.json` | nested — `PostToolUse` → `matcher` → `hooks[]` with one shell-string `command` using `${CLAUDE_PLUGIN_ROOT}` | Claude Code, Codex | Both default to this path. Claude Code reads it **even when the manifest points elsewhere** (verified from a `--debug` session), so it has to be in this schema. Codex provides `CLAUDE_PLUGIN_ROOT` as an alias of `PLUGIN_ROOT`. Matcher covers Claude Code's `Write|Edit|MultiEdit|NotebookEdit` and Codex's `apply_patch`. |
| `hooks/cursor.json` | flat — `"version": 1`, `afterFileEdit` → `command` | Cursor | `.cursor-plugin/plugin.json` points at it. Cursor's docs are explicit that a manifest `hooks` field replaces default discovery. The command is a plugin-relative script path, the form Cursor's own examples use; `${CURSOR_PLUGIN_ROOT}` is the documented alternative if the live install shows the relative path resolving elsewhere. |

The cost of this split is that community scanners that look only for `hooks/hooks.json` (cursor.directory) will report a `PostToolUse` hook rather than Cursor's `afterFileEdit`. Cursor itself reads the manifest. The previous layout — Cursor's schema at the default name — produced `[WARN] hooks.afterFileEdit: unknown hook event` on every Claude Code session start for every user.

**Loading rules that no schema check catches.** Each was found by installing the plugin and inspecting `claude plugin details` or a `--debug` session log, and each is now enforced by `scripts/validate.py`:

| Rule | Wrong form | Symptom |
|------|-----------|---------|
| MCP is discovered only from a dot-prefixed `.mcp.json` at the plugin root | `mcp.json` + `"mcpServers": "./mcp.json"`, or an inline object | `MCP servers (0)` — the whole point of the plugin, absent |
| Claude Code discovers MCP only from a dot-prefixed `.mcp.json` at the plugin root | `mcp.json` + `"mcpServers": "./mcp.json"`, or an inline object | `MCP servers (0)` |
| A hook `command` must be one shell string | `["python3", "..."]` | `Hooks (0)` |
| Claude Code reads `hooks/hooks.json` regardless of the manifest `hooks` pointer | Cursor's schema at the default name | `WARN unknown hook event` every session |
| Claude Code treats `commands/` as deprecated and also exposes each skill as a slash command | `commands/` + `skills/` with the same names | `Skills (4)` with duplicate names; `"commands": []` in `.claude-plugin/plugin.json` suppresses the deprecated dir |
| Claude Code dedupes a plugin MCP server against a manually-configured server with the same URL | — | `Suppressing plugin MCP server … duplicates manually-configured` — correct behaviour, and why a developer machine with the server in `~/.claude.json` will not show the plugin's copy |
| Codex's validator rejects any key but `mcpServers` in `.mcp.json` | `$schema` in `.mcp.json` | Codex validation fails |
| Cursor, Claude Code, Codex, and Agent Plugins all want manifest paths to start with `./` | `"skills": "skills"` | Undefined per host; normalised everywhere |

So the repo ships **both** `mcp.json` (Agent Plugins 1.0, Cursor, VS Code) and `.mcp.json` (Claude Code, Codex, Devin, Copilot CLI) with the same `mcpServers` map; the dotted file omits `$schema`. `validate.py` compares the maps rather than the bytes.

**Transport type.** Both files declare `"type": "streamable-http"`, the Agent Plugins 1.0 value. Claude Code accepts it and normalises to HTTP (verified: a project `.mcp.json` with both `streamable-http` and `http` lists both as `(HTTP)`). Cursor's native remote-server shape is `{"url": …}` with no `type`; whether Cursor's plugin loader tolerates the extra key is unverified until a live install.

So the repo ships **both** `mcp.json` (Agent Plugins 1.0 and Cursor) and an identical `.mcp.json` (Claude Code). `scripts/validate.py` fails if they diverge or if a hook command is a list. Codex and Copilot support hooks too; their schemas are not documented well enough to write blind, so they are left off rather than guessed.
**Windows.** The hook is a Python 3 script. Claude Code and Codex invoke it as `python3 "${CLAUDE_PLUGIN_ROOT}/…"`, which needs `python3` on `PATH` — present with the Microsoft Store Python, absent by default with the python.org installer, which ships `python` and `py`. Cursor invokes it by path, which relies on the shebang and does not work on Windows outside a POSIX shell. Cursor's own hook examples have the same property. Because the hook is advisory and exits 0, a Windows user loses the pre-deploy warnings and nothing else. Copilot's hook schema has separate `bash` and `powershell` commands — the only host that solves this — and is recorded below for when a plugin-root variable is confirmed for it.

**Copilot.** VS Code reads portable `skills/` and `mcp.json` from the root manifest, but custom agents only from `com.github.copilot/agents/*.agent.md`. That file is generated from `agents/` by `scripts/gen-host-components.py`, and `validate.py` fails if it drifts. Copilot CLI's own plugin reference lists agents, skills, hooks, MCP, and LSP — no commands or rules — so nothing is mirrored for those.
**Copilot.** VS Code auto-detects the format from the root manifest: a `plugin.json` carrying the Agent Plugins `$schema` is read as Agent Plugins 1.0, so skills come from `skills/`, MCP from `mcp.json`, and Copilot-specific content from `com.github.copilot/` — which VS Code documents as holding `agents/`, `hooks/`, `commands/`, and `rules/`. Custom agents are `com.github.copilot/agents/*.agent.md` (frontmatter `name`, `description`, optional `tools`), generated from `agents/` by `scripts/gen-host-components.py` and drift-checked by `validate.py`. `commands/` and `rules/` are not mirrored: VS Code's native formats there are `.prompt.md` and `.instructions.md` with `applyTo`, not the `.md`/`.mdc` this repo carries, and a wrong-format mirror is worse than none. Copilot CLI hooks are `{"version": 1, "hooks": {"postToolUse": [{"type": "command", "bash": "…", "powershell": "…"}]}}` with a camelCase `toolArgs` payload — schema verified, but no documented plugin-root variable for the command, so not shipped.

**Devin.** Devin CLI shipped plugins (closed beta). It reads `.devin-plugin/plugin.json`, falls back to `.claude-plugin/plugin.json` or the root `plugin.json`, and honors Agent Plugins 1.0 including `${PLUGIN_ROOT}`. It also reads `rules/` and `agents/`, so Devin gets more of this plugin than Codex does.
**Devin.** Devin CLI shipped plugins (closed beta). It reads `.devin-plugin/plugin.json`, falls back to `.claude-plugin/plugin.json` or the root `plugin.json`, and honors Agent Plugins 1.0 including `${PLUGIN_ROOT}`. Documented manifest keys are the metadata set plus `skills`, `mcpServers`, `requiredPlugins`, `optionalPlugins`, `forbiddenPlugins` — `validate.py` rejects anything else, which is why the earlier `agentSubagents` key was dropped; Devin reads `agents/<name>.md` by convention. MCP precedence is `.mcp.json`, then `mcp.json`, then manifest paths. Devin reads hooks from `hooks.json` at the plugin **root**, not `hooks/`, so it gets no hook from this repo; its `rules/` use Windsurf-style trigger frontmatter, so `rules/nexlayer-yaml.mdc` (Cursor frontmatter) may load without its glob trigger.

A client with an MCP marketplace but no plugin format has nothing here to package: the skills do not transfer, so it gets the tools and none of the judgment. That is the whole Windsurf / Cline / Roo / Kilo row above.

Expand Down
Loading
Loading