diff --git a/CHANGELOG.md b/CHANGELOG.md
index c261c09..4ee3901 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 0.1.0-canary.6
+
+- Fix the Claude plugin icon being rejected as `ICON_INVALID`: ship the vllnt logo as `.claude-plugin/icon.png`, named by `icon` in `plugin.json`, instead of an SVG that embedded the PNG.
+
- Remove the previous marketplace name from the README; the migration note stays in this changelog.
## 0.1.0-canary.5
diff --git a/README.md b/README.md
index ae20711..0195ae8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Then start a new chat and invoke a namespaced skill such as `/vstack:plan-work`.
| `upstream.lock.json` | Trusted upstream repository and immutable commit |
| `VERSION` | Shared distribution version |
| `scripts/sync.py` | Standard-library generator and read-only drift check |
-| `assets/vllnt-logo.png` | vllnt logo; the generator wraps it as the Claude package's `.claude-plugin/icon.svg` |
+| `assets/vllnt-logo.png` | vllnt logo; the generator ships it as the Claude package's `.claude-plugin/icon.png`, named by `icon` in `plugin.json` |
| `plugins/claude/` | Generated Claude Code native plugin |
| `plugins/codex/` | Generated portable Agent Plugin for Codex |
| `plugins/cursor/` | Generated Cursor native plugin and principles rule |
@@ -149,7 +149,7 @@ Automatic repository synchronization was independently verified on 2026-09-21. A
Live testing caught a generated-help false hold and post-push PR-head propagation timing; fixes were reviewed and merged through PRs #5 and #6, with 37 regression tests passing. The fresh PR #7 case required neither manual metadata repair nor manual merge. Daily fallback is configured; its timer was not waited for. Push delivery, manual resends, automatic merging, and manual no-op execution were observed.
-Claude Code 2.1.283 (2026-09-27): `claude plugin validate` passes for the catalog and plugin; a local-directory marketplace install into an isolated configuration installed the `vstack` plugin with all 22 skills, and a maintainer session with the local install listed every `vstack:*` skill. After `vllnt/stack` became public (merge `c3ad08c08fe460ed4f2e1a40630afe4001ddd23c`), an anonymous clone succeeded, and the CLI equivalents `claude plugin marketplace add vllnt/stack` plus the matching `claude plugin install` command in a clean, credential-free configuration installed the plugin from the GitHub source; a headless session there listed exactly the 22 `vstack:*` skills. The same commands after merge `915a4d0d519d726f84185ec698b3ed3f4c925cb8` installed it again with its icon, updated description, and 22 skills. The in-chat `/plugin` forms were not run separately. The marketplace was later renamed to `vllnt`; see the changelog for migrating earlier installs. Skill invocation behavior (including `plan-work` staying read-only), automatic skill selection, and the rest of the acceptance checklist remain unverified.
+Claude Code 2.1.283 (2026-09-27): `claude plugin validate` passes for the catalog and plugin; a local-directory marketplace install into an isolated configuration installed the `vstack` plugin with all 22 skills, and a maintainer session with the local install listed every `vstack:*` skill. After `vllnt/stack` became public (merge `c3ad08c08fe460ed4f2e1a40630afe4001ddd23c`), an anonymous clone succeeded, and the CLI equivalents `claude plugin marketplace add vllnt/stack` plus the matching `claude plugin install` command in a clean, credential-free configuration installed the plugin from the GitHub source; a headless session there listed exactly the 22 `vstack:*` skills. The same commands after merge `915a4d0d519d726f84185ec698b3ed3f4c925cb8` installed it again with its icon, updated description, and 22 skills. The in-chat `/plugin` forms were not run separately. The marketplace listing later rejected that SVG icon (a PNG embedded in SVG) as `ICON_INVALID`; the replacement `.claude-plugin/icon.png` passes `claude plugin validate --strict`, but its marketplace acceptance and rendering remain unverified. The marketplace was later renamed to `vllnt`; see the changelog for migrating earlier installs. Skill invocation behavior (including `plan-work` staying read-only), automatic skill selection, and the rest of the acceptance checklist remain unverified.
**Codex and Cursor installation, and startup/context-loss behavior in every host, remain unverified.** The repository is public and installable as a GitHub marketplace. A Claude plugin directory submission is in validation and is not listed yet. Canary builds are published as [GitHub prereleases](https://github.com/vllnt/stack/releases); releases are created manually. No automatic Claude/Codex principle loading exists.
diff --git a/VERSION b/VERSION
index 20eaaf3..8a85b8d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0-canary.5
+0.1.0-canary.6
diff --git a/plugins/claude/.claude-plugin/icon.png b/plugins/claude/.claude-plugin/icon.png
new file mode 100644
index 0000000..10b7149
Binary files /dev/null and b/plugins/claude/.claude-plugin/icon.png differ
diff --git a/plugins/claude/.claude-plugin/icon.svg b/plugins/claude/.claude-plugin/icon.svg
deleted file mode 100644
index e112555..0000000
--- a/plugins/claude/.claude-plugin/icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json
index f7edd43..598afe1 100644
--- a/plugins/claude/.claude-plugin/plugin.json
+++ b/plugins/claude/.claude-plugin/plugin.json
@@ -4,6 +4,7 @@
},
"description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).",
"homepage": "https://vllnt.com",
+ "icon": "./.claude-plugin/icon.png",
"keywords": [
"workflows",
"engineering",
@@ -13,5 +14,5 @@
"license": "MIT",
"name": "vstack",
"repository": "https://github.com/vllnt/stack",
- "version": "0.1.0-canary.5"
+ "version": "0.1.0-canary.6"
}
diff --git a/plugins/claude/SOURCE.json b/plugins/claude/SOURCE.json
index ba64368..9368076 100644
--- a/plugins/claude/SOURCE.json
+++ b/plugins/claude/SOURCE.json
@@ -1,7 +1,7 @@
{
"files": {
- ".claude-plugin/icon.svg": "0b4b639f6df841b4904ec0cd9f6d38131c6373ce4a52a1e7fe6452a37598d7f1",
- ".claude-plugin/plugin.json": "075f3e05fe3733d2930d0aaaf378d02758436dfa4f4006c34eda3c505b48e1ef",
+ ".claude-plugin/icon.png": "71502ebbc531cc46866dffe33dae3207ad22cf529f801ec971bb69114c064608",
+ ".claude-plugin/plugin.json": "60311ca1334975967e4ae9ae7710b7b57a9a854f35b5fff3b8da26a008e8c63b",
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
"skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0",
diff --git a/plugins/codex/SOURCE.json b/plugins/codex/SOURCE.json
index 4c34085..3513f3a 100644
--- a/plugins/codex/SOURCE.json
+++ b/plugins/codex/SOURCE.json
@@ -1,7 +1,7 @@
{
"files": {
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
- "plugin.json": "c78e76ede21d79745dc7572f7ba3f563d9296d9d1c3404cedc217f96597f320b",
+ "plugin.json": "1ba701827e2e216e367635d59628b4e5e6d599090dc2ea73bd0a2afe996aff2b",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
"skills/build-landing-page/SKILL.md": "b08e4963d6149f3647b28a0d9dccdc8793812abab95a1d081d93699e142de0f0",
"skills/build-landing-page/references/copywriting-formulas.md": "6d19f6166e1badcfdda0af4f8b303160e94ee6e6149321157e12849654cc013e",
diff --git a/plugins/codex/plugin.json b/plugins/codex/plugin.json
index c7cc4fd..499ffaf 100644
--- a/plugins/codex/plugin.json
+++ b/plugins/codex/plugin.json
@@ -6,5 +6,5 @@
"description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).",
"license": "MIT",
"name": "vstack",
- "version": "0.1.0-canary.5"
+ "version": "0.1.0-canary.6"
}
diff --git a/plugins/cursor/.cursor-plugin/plugin.json b/plugins/cursor/.cursor-plugin/plugin.json
index 3f06dc7..72b4293 100644
--- a/plugins/cursor/.cursor-plugin/plugin.json
+++ b/plugins/cursor/.cursor-plugin/plugin.json
@@ -5,5 +5,5 @@
"description": "Evidence-led engineering workflows to plan, build, review, and ship software with AI agents. Part of the vllnt universe: open, sovereign tools for freedom by design (vllnt.com).",
"license": "MIT",
"name": "vstack",
- "version": "0.1.0-canary.5"
+ "version": "0.1.0-canary.6"
}
diff --git a/plugins/cursor/SOURCE.json b/plugins/cursor/SOURCE.json
index c70f232..036df41 100644
--- a/plugins/cursor/SOURCE.json
+++ b/plugins/cursor/SOURCE.json
@@ -1,6 +1,6 @@
{
"files": {
- ".cursor-plugin/plugin.json": "63c0531e7ec6ff67f7fecd54ee886f3d10706dbab51820807b373378ce7ee4b3",
+ ".cursor-plugin/plugin.json": "b8908a118e691f11f901524b4f28b372cf35699b60392575c2a4a5dfaf3923a9",
"LICENSE": "24ba061a4603738ddd5105ef4abc198eeded5ac7efce23b360561816f4dd6559",
"rules/vstack-principles.mdc": "5234bc0506e0a328d6163cd83785d2f37dd1fc23ef84287d71e03b92459e064a",
"skills/build-landing-page/README.md": "46a280eadac8962180cf3f0c42eb0e845a3b0156a644569182d932881e1d51f8",
diff --git a/scripts/sync.py b/scripts/sync.py
index 6d52e6e..d098b54 100644
--- a/scripts/sync.py
+++ b/scripts/sync.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import argparse
-import base64
import gzip
import hashlib
import io
@@ -220,17 +219,16 @@ def payload(files: dict[str, bytes]) -> tuple[dict[str, bytes], bytes]:
return skills, rule.encode()
-def icon_svg(png: bytes) -> bytes:
- """Wrap a square PNG in the SVG file Claude's plugin directory looks for."""
- if png[:8] != b"\x89PNG\r\n\x1a\n":
+def icon_png(png: bytes) -> bytes:
+ """Check the PNG shipped as Claude's plugin icon; listings reject SVGs with embedded images."""
+ if png[:8] != b"\x89PNG\r\n\x1a\n" or png[12:16] != b"IHDR":
raise Invalid("Claude icon must be a PNG")
width, height = struct.unpack(">II", png[16:24])
if width != height or width < 128:
raise Invalid("Claude icon must be square and at least 128px")
- data = base64.b64encode(png).decode()
- return (f'\n').encode()
+ if len(png) > MAX_FILE:
+ raise Invalid("Claude icon must be at most 2 MiB")
+ return png
def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]:
@@ -244,14 +242,15 @@ def build(files: dict[str, bytes], lock: dict, version: str) -> dict[str, bytes]
"author": {"name": "vllnt"}, "license": "MIT"}
if host == "claude":
manifest.update({"homepage": HOMEPAGE, "repository": REPOSITORY,
- "keywords": ["workflows", "engineering", "principles", "skills"]})
+ "keywords": ["workflows", "engineering", "principles", "skills"],
+ "icon": "./.claude-plugin/icon.png"})
if host == "codex":
manifest["$schema"] = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
manifest_path = {"claude": ".claude-plugin/plugin.json", "codex": "plugin.json",
"cursor": ".cursor-plugin/plugin.json"}[host]
package = {**skills, "LICENSE": files["LICENSE"], manifest_path: encoded(manifest)}
if host == "claude":
- package[".claude-plugin/icon.svg"] = icon_svg(CLAUDE_ICON.read_bytes())
+ package[".claude-plugin/icon.png"] = icon_png(CLAUDE_ICON.read_bytes())
if host == "cursor":
package["rules/vstack-principles.mdc"] = rule
package["SOURCE.json"] = encoded({"upstream": lock, "files": {
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 4257f1c..8ad87f5 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -54,8 +54,9 @@ def test_deterministic_standalone_packages_and_provenance(self):
self.assertEqual(package["skills/test-work/SKILL.md"], files["workflows/test-work/SKILL.md"])
self.assertEqual("rules/vstack-principles.mdc" in package, host == "cursor")
self.assertEqual(package["LICENSE"], files["LICENSE"])
- self.assertEqual(".claude-plugin/icon.svg" in package, host == "claude")
- self.assertIn(b"data:image/png;base64,", first["plugins/claude/.claude-plugin/icon.svg"])
+ self.assertEqual(".claude-plugin/icon.png" in package, host == "claude")
+ self.assertNotIn(".claude-plugin/icon.svg", package)
+ self.assertEqual(first["plugins/claude/.claude-plugin/icon.png"], sync.CLAUDE_ICON.read_bytes())
claude = json.loads(first[".claude-plugin/marketplace.json"])
codex = json.loads(first[".agents/plugins/marketplace.json"])
cursor = json.loads(first[".cursor-plugin/marketplace.json"])
@@ -70,15 +71,17 @@ def test_deterministic_standalone_packages_and_provenance(self):
("cursor", "plugins/cursor/.cursor-plugin/plugin.json"))}
self.assertEqual(manifests["claude"]["repository"], "https://github.com/vllnt/stack")
self.assertEqual(manifests["claude"]["homepage"], "https://vllnt.com")
+ self.assertEqual(manifests["claude"]["icon"], "./.claude-plugin/icon.png")
+ self.assertNotIn("icon", manifests["codex"])
self.assertNotIn("repository", manifests["codex"])
self.assertNotIn("repository", manifests["cursor"])
- def test_claude_icon_requires_square_png(self):
+ def test_claude_icon_requires_small_square_png(self):
png = lambda w, h: b"\x89PNG\r\n\x1a\n" + b"\0\0\0\rIHDR" + struct.pack(">II", w, h)
- self.assertIn(b'viewBox="0 0 256 256"', sync.icon_svg(png(256, 256)))
- for bad in (png(256, 128), png(64, 64), b"" + bytes(32)):
+ self.assertEqual(sync.icon_png(png(256, 256)), png(256, 256))
+ for bad in (png(256, 128), png(64, 64), b"" + bytes(32), png(256, 256) + bytes(sync.MAX_FILE)):
with self.assertRaises(sync.Invalid):
- sync.icon_svg(bad)
+ sync.icon_png(bad)
def test_added_and_removed_workflows(self):
files = source()