codex-experience-kit
v0.6.12
Published
Standalone and embeddable Experience SDK, CLI, sandbox runtime, and project toolchain for Codex desktop.
Downloads
3,708
Readme
Codex Experience Kit
Codex Experience Kit is a standalone CLI and embeddable SDK for sandboxed HTML experiences in Codex desktop. Appearance is one capability; an Experience may also contain animation, local state, cross-surface signals, task-aware visuals, lifecycle reactions, and explicitly authorized host actions.
An external host is not required. The npm package itself can build, preview, apply, hot-refresh, patch, inspect, and cancel an Experience. It has no daemon, separate login, profile, or Dock icon. Applications may embed the same public APIs as an optional UI.
Upgrading from the deprecated codex-theme-kit package requires a project-format and API migration, not only a dependency rename. See Migrating from codex-theme-kit.
Create a project
npx codex-experience-kit init ./example-experience \
--id example.experience \
--name "Example Experience"
cd example-experience
npm install
npm run devReact is the default. Choose Vue explicitly with --framework vue; --framework react is also accepted for automation. init creates a complete framework project, not a loose HTML file:
example-experience/
├── experience.manifest.json
├── experience.config.json
├── package.json
├── README.md
├── AGENTS.md
├── EXPERIENCE-BRIEF.md
├── docs/AI-GENERATION.zh-CN.md
├── src/
│ ├── main.tsx
│ ├── codex-experience.d.ts
│ └── surfaces/
│ ├── background/
│ │ ├── index.tsx
│ │ └── styles.css
│ └── overlay/
│ ├── index.tsx
│ └── styles.css
├── assets/
└── dist/ # generated by npm run buildCommands:
npm run dev: bundle React/Vue components, watch only real source/config/asset inputs, and serve a stable synthetic Codex preview. Its toolbar can independently show/hide underlay and overlay, simulate active/background task events, explicitly apply the current build to Codex, or restore the official view.npm run dev:native: open the same synthetic preview in a separate Electron window. Afternpm install --save-dev electron, unrestricted remote pages useWebContentsViewinstead of iframe embedding.npm run build: bundle framework source with the project-local Vite and compile it into the fixed runtime contract indist/.npm run check: validate the built package, surfaces, assets, permissions, and sandbox rules.npm run pack: create an importable ZIP inreleases/.npm run apply: build and directly apply the current project to Codex.npm run appearance -- --seed '#008577' --appearance dark: patch only the live appearance.npm run status: inspect the package-owned runtime state.npm run cancel: restore the official Codex view.
Secondary-account data is persistent. codex-experience instance status inspects the fixed secondary slot, codex-experience instance open reopens it without applying an Experience, and codex-experience instance install-launcher creates ~/Applications/Codex Secondary.app for Finder, Spotlight, or Dock access after reboot.
The generated documentation tells an AI what it must ask the user for, how placement works, which assets are missing, and how to verify Light/Dark, interaction, motion, and reading safety.
Opening the preview is synthetic and does not connect to Codex. Underlay and Overlay visibility controls affect only the preview and never alter the built Experience. Clicking Apply to Codex is an explicit real operation: the preview rebuilds the project, passes the same generated Light/Dark tokens to the package-owned runtime, and asks for confirmation if the first connection requires a restart. Restore transactionally cancels the active Experience.
Apply directly to Codex
From an initialized Experience project:
npm run applyThe first connection may require one confirmed Codex restart. The command never restarts automatically; after saving current work, explicitly grant that operation:
npm run apply -- --allow-restartLater npm run apply calls rebuild the project and transactionally hot-refresh its linked dist/. They reuse the existing Codex profile, login, tasks, and conversations and do not require an external host or another restart.
The package CLI also works outside an authoring project:
codex-experience install ./releases/example.experience-0.1.0.zip
codex-experience list
codex-experience apply example.experience --seed '#6750A4' --appearance light
codex-experience appearance --seed '#008577' --appearance dark
codex-experience status
codex-experience cancelRuntime state and immutable/development snapshots default to ~/Library/Application Support/CodexExperienceKit. Override it with --library <directory> when isolation is required.
Placement contract
Authors write ordinary React or Vue components. Placement is declarative in experience.config.json; the compiler generates runtime HTML:
{
"authoring": {
"framework": "react",
"entry": "main.tsx",
"surfaces": [
{ "target": "app-shell", "plane": "underlay", "interaction": "passthrough" },
{ "target": "titlebar", "plane": "overlay", "interaction": "scoped" }
]
}
}The entry registers components by the same plane:target key. Each surface owns a directory containing its component and CSS, so background and overlay can evolve independently. Users do not create codex-experience-surface, iframe, or runtime HTML by hand. Existing unambiguous v1 vanilla source projects remain buildable.
Targets are app-shell, navigation, titlebar, workspace, home, conversation, composer, modal, and floating-window.
floating-window is a host-owned top-level overlay in its own sandbox iframe. It must use interaction: "scoped" or "interactive". Keep its trigger in the original semantic surface and coordinate the separate window through window.codexExperience.signals; do not simulate this boundary with a larger z-index inside one surface.
Planes are:
underlay: below native Codex content and always pointer-passthrough.overlay: above native content. Usepassthroughfor decoration,scopedfor isolated buttons/panels, andinteractiveonly when the whole semantic surface belongs to the Experience.
In a scoped overlay, register only actual hit targets with window.codexExperience.interaction.register(element, { padding, shape }). shape is rect by default, rounded to follow the element's computed top-left border radius, or circle for circular floating controls. The host derives a disjoint hit-test path from those elements; everything else continues to click through to native Codex. Destroy the returned handle when the element unmounts.
The order is floating-window → ordinary overlay → native Codex → underlay → native region background. A project may provide only one surface. Missing route or modal targets stay pending and mount when the semantic region exists; floating-window is always available at app-window scope.
Browser capability API
Experience JavaScript runs in a sandboxed iframe without same-origin access. It receives window.codexExperience with only:
- environment: mode, target, plane, appearance, reduced-motion state, and the selected
remoteContentBackend; - complete appearance token modes after
appearance.tokenspermission; - Experience-local cross-surface signals;
- declared semantic host actions after
host.actionspermission; - host-managed remote pages after
remote.webview, a declared security mode, and an interactive overlay; - a sanitized selected-task snapshot after
codex.context.active, with an optional display name only aftercodex.context.metadata; - sanitized task/turn lifecycle events after
codex.events.lifecycle; - lifecycle notifications.
It cannot access Codex DOM, the parent document, Node.js, Electron, CDP, the filesystem, arbitrary network APIs, or arbitrary host methods. Package resources are compiled into self-contained HTML. remote.webview does not enable Electron's <webview> tag: author code sends validated mount, layout, navigation, reload, and destroy requests to a package-controlled sibling host. The Experience frame remains opaque and cannot reach the host or remote DOM.
Relayed cross-surface signals include host-authored source metadata with the semantic target, plane, and source-surface bounds in app coordinates. This allows a control in workspace or navigation to anchor a separate floating-window without reading Codex DOM or guessing region widths.
Remote content declares one of three policies:
strictis the default: exact HTTPS origins, credentialless/no-referrer sandbox, and no popups, downloads, device permissions, or top navigation.permissiveallows arbitrary HTTP/HTTPS navigation while retaining the credentialless/no-referrer sandbox and the remaining restrictions.unrestricteduses a direct host-owned sibling iframe in browser/CDP providers, or an isolated nativeWebContentsViewin an Electron host. It is critical risk and activates only when the project requests it and the host separately passesallowUnrestrictedRemoteContent: trueor the CLI receives--allow-unrestricted-remote-content.
Even unrestricted mode never exposes Node.js, Electron, CDP, file:, or javascript: to Experience code. Browser/CDP providers still cannot bypass a site's iframe X-Frame-Options or frame-ancestors policy. A native WebContentsView is a top-level web content, so it is not subject to iframe-only embedding rules; it still obeys normal network, page CSP, navigation, permission, download, mixed-content, and operating-system controls. Never grant unrestricted mode to an untrusted project.
For a page that refuses iframe embedding:
npm install --save-dev electron
npm run dev:native -- --allow-unrestricted-remote-contentAuthor code keeps one API and may provide a browser-only fallback:
window.codexExperience?.webviews.mount(container, {
url: "https://www.baidu.com/",
iframeFallbackUrl: "https://m.baidu.com/",
title: "Baidu",
});The package selects url for Electron native preview and iframeFallbackUrl for ordinary browser/CDP preview. Native views belong to their Electron process; they cannot be attached across processes to the official Codex window. Consequently, direct application to official Codex continues to use the iframe provider and should use the fallback when the primary site blocks embedding.
Codex context and lifecycle events
Task-aware Experiences opt in explicitly:
{
"permissions": ["codex.context.active", "codex.events.lifecycle"]
}The API exposes only identifiers, selected state, idle|working|waiting|completed|failed|interrupted, unread state, and timestamps. It never exposes prompts, responses, cwd, credentials, or general methods that control Codex; task display names require the separate metadata permission. With host.actions, codex.window.open may open the shared Codex home view or a validated local thread in a separate native window. On macOS, the reserved isolated-instance actions instead start a separate process with fixed Kit-managed Chromium and CODEX_HOME directories for another account; project code cannot provide executable paths, arguments, environment variables, or profile paths.
codex.instance.configure enables the package-owned transfer catalog and configured launch request. Known capabilities—config/MCP registration, workspace/project list, Skills, Plugins, rules, Hooks, automations, Memories, and Pets—default to selected. Conversations default off and are presented as a project-grouped tree: users may select a complete group or expand it and choose individual conversations, with logical sizes at both levels. Unknown top-level CODEX_HOME data appears as an unselected item with its absolute source path. Account authentication, Chromium/browser identity, OAuth locks, IPC, active writer/process state, logs, and worktrees are never selectable. Selecting conversations additionally requires codex.conversations.sync and a native confirmation. Selective migration merges only chosen thread rows and their local rollouts/resources. Copied config.toml and Hook paths rooted inside the primary CODEX_HOME are rebased to the secondary directory; secondary credentials are preserved.
const snapshot = await window.codexExperience?.context.getSnapshot();
const stopContext = window.codexExperience?.context.subscribe((next) => {
document.documentElement.dataset.thread = next.activeThreadId ?? "home";
});
const stopEvents = window.codexExperience?.events.subscribe((event) => {
if (event.type === "turnCompleted" && event.threadId !== snapshot?.activeThreadId) {
// Render a local badge or animation for the completed background task.
}
});The ordinary preview supplies two synthetic tasks and controls for switching the active task and completing the other task. Reusable preview hosts can pass codexContext, call setCodexContext() / emitCodexEvent(), or provide a CodexContextSource.
Preview control and version endpoints support browser preflight from loopback origins such as localhost, 127.0.0.1, and [::1], including different local ports. Control requests still require the per-process random token embedded in the preview document. Non-loopback origins, null origins, wildcard CORS, and credentialed origins are rejected.
Direct Codex application always includes a renderer provider for the currently selected task and visible task-status transitions. A host that owns an explicit Codex App Server WebSocket may add authoritative lifecycle events:
import { CodexAppServerContextProvider, CodexExperienceRuntime } from "codex-experience-kit/node";
import { CodexContextService } from "codex-experience-kit/service";
const context = new CodexContextService(new CodexAppServerContextProvider({
webSocketUrl: "ws://127.0.0.1:4500",
}));
await context.start();
const runtime = new CodexExperienceRuntime({ contextSource: context });
try {
await runtime.apply("./example-experience");
} finally {
await runtime.shutdown();
await context.stop();
}The endpoint is never discovered automatically. Loopback is required by default; remote endpoints require an explicit host option and should use wss: plus authentication. App Server WebSocket transport is experimental, so the renderer provider remains the safe fallback for official Codex desktop.
Optional host integration
npm install --save-exact [email protected]import { ExperienceEngine } from "codex-experience-kit/node";
import { generateAppearanceTokens } from "codex-experience-kit/utils";
const engine = new ExperienceEngine({ libraryPath });
await engine.initialize();
const tokens = generateAppearanceTokens({ seed: "#6750A4" }).modes;
await engine.applyProject("example.experience", {
tokens,
appearance: "light",
allowRestart: true,
});
await engine.patchTokens(
generateAppearanceTokens({ seed: "#008577" }).modes,
"dark",
);
await engine.cancelProject();For a default-path, high-level Node API that matches the standalone CLI:
import { CodexExperienceRuntime } from "codex-experience-kit/node";
const runtime = new CodexExperienceRuntime();
await runtime.apply("./example-experience", { allowRestart: true });
await runtime.patchAppearance({ seed: "#008577", appearance: "dark" });
await runtime.cancel();
await runtime.shutdown();If a trusted project explicitly requests unrestricted remote content, the embedding host must acknowledge that critical risk separately:
const runtime = new CodexExperienceRuntime({
security: { allowUnrestrictedRemoteContent: true },
});Do not derive this host grant from the manifest itself. Display remoteContentRisk from project, catalog, or preview results before asking the user to enable it.
For a synthetic renderer preview:
import { mountExperienceProjectPreview } from "codex-experience-kit/preview";
const preview = mountExperienceProjectPreview(host, project, {
tokens,
appearance: "light",
view: "task",
});Normal import copies a validated immutable snapshot. Direct project application builds with the package-owned compiler, links the generated dist/, caches the last valid snapshot, and changes only during an explicit apply. An optional host must never execute scripts from a third-party authoring project.
Codex connection
Application uses an explicit loopback CDP connection to the official signed Codex process. Establishing that connection may require one confirmed restart. The runtime selects the primary Codex renderer rather than auxiliary windows such as the avatar overlay, and can safely adopt an already-running loopback CDP session after verifying both the signed process and endpoint ownership. Once connected, Experience switching, token patching, selected-task context, renderer-visible lifecycle updates, and cancellation are hot operations and use the original Codex profile, login, tasks, and conversations. An optional App Server provider accepts only an explicit endpoint and is never discovered or attached during tests.
All real-Codex mutations are transactional. The normal suite uses synthetic DOM and CDP servers. A separate external-CDP fixture exercises an isolated Electron renderer with Codex-equivalent CSP and the complete demo artifact. Neither path connects to the user's running Codex, and neither is described as a successful official-Codex application.
Acknowledgements
Renderer selector research and the explicit loopback-CDP lifecycle were informed by Codex Dream Skin, Copyright (c) 2026 Codex Dream Skin Studio contributors, under the MIT License. Codex Experience Kit is an independently written SDK and does not bundle Dream Skin artwork, presets, theme CSS, injector source, or application binaries. The complete attribution and upstream license are retained in NOTICE.md.
Codex Experience Kit is an independent, unofficial project. It is not affiliated with or endorsed by OpenAI. Codex and related marks belong to their respective owners.
See migration from codex-theme-kit, direct usage, architecture, project format, runtime contract, optional host integration, AI rules, open-source compliance, and security.
