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).
Governance
Section titled “Governance”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:andx-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:
- Format validity — obeys this spec (the reference validator,
skin-format.mjs). - Content safety — token-only CSS, no remote resources, no Codex internals (§5).
- Authenticity — signed by po9 (future;
verification/envelope). Absence is normal. - Approval status — listed/approved in the po9 Catalog (server-side, revocable).
- 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.
1. The hero is the highlight
Section titled “1. The hero is the highlight”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.
2. Document shape
Section titled “2. Document shape”{ "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 integer1.manifest,css— required.art— present iffmanifest.artnames a file; otherwise omit it and setmanifest.arttonull.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.
3. manifest
Section titled “3. manifest”{ "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.
4. art payload (the hero image)
Section titled “4. art payload (the hero image)”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.arttonulland omitart. (A skin with no image still works, but see §1 — the hero is the highlight.)
5. The css contract
Section titled “5. The css contract”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):
- Scope. The CSS MUST be scoped to
codedrobe-codex-skin— write your rules underhtml.codedrobe-codex-skin { … }(or:root.codedrobe-codex-skin). The literal stringcodedrobe-codex-skinMUST appear in the CSS. - 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. - No remote. MUST NOT use
@importor remoteurl(...).url(data:...)is allowed (e.g. an inline SVG), remote URLs are not. - Size. ≤ 1 MB.
- English only. No Han (Chinese) characters.
5.1 Token vocabulary
Section titled “5.1 Token vocabulary”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.
6. Complete minimal example
Section titled “6. Complete minimal example”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==" }}7. Producing and using a skin
Section titled “7. Producing and using a skin”- By hand or with AI: write the JSON above, embed your hero image as base64,
save with a
.po9-skinextension. 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 themanifest.json+theme.css, thennpm run creator:validateandnpm run creator:pack. The kit runs the exact rules in this spec and emits a.po9-skinfor 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”).
8. Compatibility badge
Section titled “8. Compatibility badge”A tool or package may display:
Compatible with po9 Skin Specification v1only 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).
9. Version history
Section titled “9. Version history”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.