Skip to content

PO9 Skin Format Specification (v1)

An open JSON format for portable workspace skins, maintained by po9.

A po9 Skin is a single JSON document. It carries a hero image, a set of color tokens, and short copy — nothing executable, nothing that reaches the network. Any tool (including an AI assistant) may produce one directly from this spec; it does not require the po9 Creator Kit. po9 owns the specification and the reference implementation, and the desktop importer is the trust boundary: a file is accepted only if it obeys every rule below.

  • Wire format token: "po9-skin" (the legacy "codex-theme" is still accepted on import for older files; new files should use "po9-skin").
  • File extension: .po9-skin
  • Container: UTF-8 JSON. Not a zip. Images are embedded as base64.
  • Reference validator: lib/skin-format.mjs. If a file passes that module, it is a valid po9 Skin. This document describes exactly what that module enforces.
  • Canonical home: https://po9.skin/specification/ — the authoritative source for this specification, its versions, and its conformance suite.
  • Official reference implementation: the po9 Creator Kit (see §7).

The po9 project maintains the specification to preserve compatibility, security, and interoperability. Third parties are encouraged to build compatible tools and packages. The specification is open for implementation, while format evolution is governed by the po9 project.

  • Maintainer — po9 (Thanachat Fugkhiew). One official specification, one official validator, one official conformance suite.
  • Decision process — po9 decides format changes directly; there is no committee or RFC process yet (deferred until external contributors exist).
  • Format evolution — only po9 may declare a new format version (e.g. schemaVersion = 2, “Specification v2”). See §0 for the two version fields.
  • Compatibility policy — the single Official Validator (lib/skin-format.mjs) is shared by the App, the Creator Kit, and CI. “Passes in the Kit but fails to import in the App” is a defect, not a variation.
  • Reserved namespace — the po9: and x-po9-* manifest keys are reserved for po9 from v1 (enforced; see §3).

0. What this format is, and what “valid” means

Section titled “0. What this format is, and what “valid” means”

po9 App is the first consumer of this format, not its owner-in-secret. Any creator, AI tool, design tool, organization, or external catalog may produce a .po9-skin from this document alone and po9 will import it. po9 governs the standard (format version, allowed tokens, security policy); it does not require files to be born inside the app.

Two independent versions — never conflate them:

Field Meaning Example
schemaVersion the format version — which revision of this spec the file follows 1
manifest.version the skin’s own version — the creator’s release number 2.3.0

“Valid” is only the first of five independent axes. A file passing this spec is a valid po9 Skin — nothing more. Do not collapse these into one verified flag; each is decided separately and at a different layer:

  1. Format validity — obeys this spec (the reference validator, skin-format.mjs).
  2. Content safety — token-only CSS, no remote resources, no Codex internals (§5).
  3. Authenticity — signed by po9 (future; verification/ envelope). Absence is normal.
  4. Approval status — listed/approved in the po9 Catalog (server-side, revocable).
  5. Entitlement — the account’s right to use it (server-side, never in the file).

A perfectly valid, safe, unsigned skin that is not in any catalog and needs no entitlement is a normal local/community skin — it works. Marketplace is one distribution channel for this format, not the format’s gatekeeper.

Conformance fixtures for axes 1–2 live in fixtures/ and are exercised by the conformance suite; every consumer shares them.


The single most visible part of a skin is the hero: a large banner at the top of the Codex home. Treat it as the star of the skin, not an afterthought. A skin with strong colors but no hero image looks unfinished.

The hero is composed from four pieces you control:

Piece Where it comes from Role
Hero image manifest.art (embedded base64 image) → exposed to CSS as --dream-art the background artwork — the visual identity of the skin
Hero title manifest.copy.heroTitle large headline over the image (defaults to “What should we build?”)
Hero tagline manifest.copy.tagline subtitle under the title
Hero framing the --dream-hero-* tokens scrim/overlay for text legibility, border, and text colors on top of the image

The base skin mounts a .dream-hero element and paints --dream-art as its background, with --dream-hero-scrim and --dream-hero-overlay layered on top so the title stays readable. Always ship an art image and set --dream-hero-scrim so the hero reads well.

Recommended hero image: a wide 16:9-ish PNG or WebP (e.g. ~1600×900), under a few MB. It is embedded in the file, so smaller is better.


{
"format": "po9-skin",
"schemaVersion": 1,
"exportedAt": "2026-07-28T00:00:00.000Z", // ISO 8601, informational
"manifest": { /* see §3 */ },
"css": "html.codedrobe-codex-skin { --dream-... }", // see §5
"art": { /* present only if manifest.art is set — see §4 */ }
}
  • format — MUST be "po9-skin" (or legacy "codex-theme").
  • schemaVersion — MUST be the integer 1.
  • manifest, css — required.
  • art — present iff manifest.art names a file; otherwise omit it and set manifest.art to null.
  • verification — optional, reserved. A po9 signature envelope (a future signing layer). Unsigned skins omit it. po9 issues no signatures yet, but the field is preserved on import and survives a re-export unchanged, so a tool may carry one through. Any other unknown top-level field is ignored.
  • Whole document MUST be ≤ 30 MB.

{
"schemaVersion": 1,
"id": "aurora-glass", // ^[a-z0-9][a-z0-9_-]*$ (case-insensitive)
"displayName": "Aurora Glass", // non-empty
"version": "1.0.0", // semantic version
"css": "theme.css", // local filename, always "theme.css" in a package
"art": "hero.png", // local image filename, or null
"attribution": { // REQUIRED
"creator": "Your name", // REQUIRED, non-empty
"rights": "Original work. You hold the rights to distribute this skin.", // REQUIRED
"collection": "Independent release", // optional
"licenseUrl": "https://..." // optional, must be https
},
"copy": { // optional but recommended for the hero
"heroTitle": "Design in aurora light.",
"tagline": "A calm, luminous workspace.",
"projectPrefix": "Project", // optional
"projectLabel": "Workspace" // optional
},
"baseTheme": { // optional — the native window appearance
"mode": "dark", // "light" | "dark"
"accent": "#7dd3fc",
"ink": "#e6edf5",
"surface": "#0f172a",
"codeTheme": "codex",
"opaqueWindows": true,
"fonts": { "macUi": "SF Pro Text", "macCode": "SF Mono", "windowsUi": "Segoe UI", "windowsCode": "Cascadia Code" }
}
}

Hard rules (enforced): schemaVersion === 1; id matches the pattern; non-empty displayName; version is a valid semver; css and art are safe local paths (no .., no absolute paths); attribution.creator and attribution.rights are non-empty strings; English only — any Han (Chinese) characters are rejected.

Reserved namespaces. Manifest keys prefixed po9: (e.g. po9:catalogId, po9:verified) or matching x-po9-* are reserved for po9 and MUST NOT be used by third-party packages. They belong to po9’s own pipeline (catalog, verification, entitlement), so reserving them from v1 prevents a future official meaning from colliding with creator data. The importer rejects any manifest that carries such a key. (This mirrors the reserved verification envelope in §2; the difference is that verification is preserved on re-export, whereas a reserved namespace key in a creator manifest is an error.)

accent, ink, surface from baseTheme are also used for the skin’s preview swatch, so set them to representative colors.


When manifest.art names a file, include the image inline:

"art": {
"filename": "hero.png",
"mimeType": "image/png",
"base64": "iVBORw0KGgo..." // the raw image bytes, base64-encoded
}
  • Allowed image types: PNG, JPEG, WebP, GIF (by extension).
  • The image is embedded, so it counts toward the 30 MB document limit — keep it lean.
  • If there is no hero image, set manifest.art to null and omit art. (A skin with no image still works, but see §1 — the hero is the highlight.)

The css string is a color palette only. It sets --dream-* tokens (and ordinary CSS properties) under the skin’s scope. It never styles Codex’s own DOM directly — the po9 base owns all Codex-DOM plumbing and changes between Codex versions, so a skin that reaches into it would break on the next Codex update.

Rules (all enforced by the importer):

  1. Scope. The CSS MUST be scoped to codedrobe-codex-skin — write your rules under html.codedrobe-codex-skin { … } (or :root.codedrobe-codex-skin). The literal string codedrobe-codex-skin MUST appear in the CSS.
  2. Token-only. MUST NOT target Codex-internal selectors. Rejected patterns include app-shell-left-panel, main-surface, home-suggestions, home-banners, home-icon, composer-surface, app-header-tint, horizontal-scroll-fade-mask, project-selector, bg-token-*, ProseMirror, and attribute selectors like [role=…], [data-testid], [data-feature], [data-message-author-role], [aria-current]. Set --dream-* tokens instead.
  3. No remote. MUST NOT use @import or remote url(...). url(data:...) is allowed (e.g. an inline SVG), remote URLs are not.
  4. Size. ≤ 1 MB.
  5. English only. No Han (Chinese) characters.

These are the tokens the base skin reads (Version 1). Any color value is fine — solid colors, rgba(), or linear-gradient(...).

Hero (the highlight): --dream-art (set automatically from your image — do not set it yourself), --dream-hero-title, --dream-hero-tagline, --dream-hero-border, --dream-hero-overlay, --dream-hero-scrim

Page & surfaces: --dream-page-bg, --dream-main-bg, --dream-header-bg, --dream-panel-bg, --dream-panel-border, --dream-panel-shadow, --dream-ink, --dream-ink-soft, --dream-scrollbar, --dream-dots

Sidebar: --dream-sidebar-text, --dream-sidebar-hover, --dream-sidebar-hover-line, --dream-sidebar-active, --dream-sidebar-active-line

Composer & editor: --dream-composer-bg, --dream-composer-border, --dream-composer-badge, --dream-composer-charm, --dream-send-bg, --dream-editor-text, --dream-caret

Cards: --dream-card-bg, --dream-card-border, --dream-card-text, --dream-card-icon

Project chips: --dream-project-bg, --dream-project-text, --dream-project-label-color

Brand & accents: --dream-brand, --dream-brand-note, --dream-brand-sub, --dream-signature, --dream-ribbon, --dream-ribbon-sub, --dream-mode-toggle, --dream-mode-toggle-mark, --dream-polaroid-charm

Unknown --dream-* tokens are harmless (ignored). Missing tokens fall back to the base skin’s defaults.


A valid, hero-first skin (art bytes elided):

{
"format": "po9-skin",
"schemaVersion": 1,
"exportedAt": "2026-07-28T00:00:00.000Z",
"manifest": {
"schemaVersion": 1,
"id": "aurora-glass",
"displayName": "Aurora Glass",
"version": "1.0.0",
"css": "theme.css",
"art": "hero.png",
"copy": { "heroTitle": "Design in aurora light.", "tagline": "A calm, luminous workspace." },
"attribution": { "creator": "Aria Studio", "rights": "Original work. You hold the rights to distribute this skin." },
"baseTheme": { "mode": "dark", "accent": "#7dd3fc", "ink": "#e6edf5", "surface": "#0f172a" }
},
"css": "html.codedrobe-codex-skin {\n color-scheme: dark;\n --dream-ink: #e6edf5;\n --dream-ink-soft: #9aa9bb;\n --dream-page-bg: linear-gradient(145deg, #0c1220, #111827);\n --dream-main-bg: #0e1524;\n --dream-panel-bg: #182131;\n --dream-panel-border: rgba(125,211,252,.24);\n --dream-hero-scrim: linear-gradient(90deg, rgba(10,15,25,.98), rgba(24,33,49,.6) 60%, transparent);\n --dream-hero-border: rgba(125,211,252,.42);\n --dream-hero-title: #eef4fb;\n --dream-hero-tagline: rgba(205,220,235,.92);\n --dream-composer-bg: #182131;\n --dream-composer-border: rgba(125,211,252,.40);\n --dream-send-bg: linear-gradient(145deg, #7dd3fc, #38bdf8);\n --dream-brand: #e6edf5;\n}",
"art": { "filename": "hero.png", "mimeType": "image/png", "base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" }
}

  • By hand or with AI: write the JSON above, embed your hero image as base64, save with a .po9-skin extension. Import it in po9 via Import — it opens the same file dialog whether the file came from the Creator Kit, an AI, or a text editor.
  • With the Creator Kit (optional SDK): npm run creator:new, edit the manifest.json + theme.css, then npm run creator:validate and npm run creator:pack. The kit runs the exact rules in this spec and emits a .po9-skin for you.

Either way, the importer enforces every rule here. A file that violates a rule is rejected with a message naming the problem (e.g. “CSS must be scoped to codedrobe-codex-skin” or “CSS must not target Codex-internal selectors”).


A tool or package may display:

Compatible with po9 Skin Specification v1

only if it passes the Official Validator / conformance suite — the fixtures in fixtures/, exercised through lib/skin-format.mjs. The badge asserts conformance to this specification, nothing more (not authenticity, approval, or entitlement — see §0).


Specification v1.0

  • Initial public release.
  • Canonical validator: lib/skin-format.mjs.
  • Creator Kit named the official reference implementation.
  • Reserved the po9: / x-po9-* manifest namespace for po9.