ULIS — Unified LLM Interface Specification
Version 1.0.0 · Source: src/schema/ · Targets: Claude Code, OpenCode, Codex, Cursor, ForgeCode · CLI: @nejcm/ulis
1. Overview
ULIS is a CLI (ulis) that lets you define AI agent configurations once and compile them into the native format expected by each supported tool. You write canonical entity definitions in .ulis/ (project) or ~/.ulis/ (global), run ulis build (or ulis install to also deploy), and get ready-to-deploy configs under <source>/generated/.
.ulis/ .ulis/generated/
├── agents/*.md ─────► ├── claude/ (agents/, commands/, rules/, settings.json, .claude.json)
├── skills/*/ ─────► ├── opencode/ (opencode.json, agents/, skills/)
│ SKILL.md ├── codex/ (config.toml, agents/*.toml, AGENTS.md)
├── mcp.yaml ─────► ├── cursor/ (agents/*.mdc, skills/, mcp.json)
│ └── forgecode/ (AGENTS.md, .forge/agents, .forge/skills, .forge/.mcp.json)
├── skills.yaml (external skill installs)
├── extensions.yaml (third-party CLI extension installs via npx/bunx)
├── permissions.yaml
└── config.yaml ◄─── version + name + optional install + runner settingsulis install deploys the generated tree to the per-platform destination (./.claude/, ./.forge/, etc.), preserving only allowlisted existing native config values or files such as MCP servers, hooks, trusted projects, selected Claude Code preferences, Codex tui, notice, and features, and ForgeCode .forge.toml.
Why it exists: Claude Code, OpenCode, Codex, Cursor, and ForgeCode all have incompatible config formats. Without ULIS you maintain separate, drift-prone config trees. ULIS keeps one source of truth and compiles it.
2. Architecture
┌─────────────────────────────────────────────────────────┐
│ Source: .ulis/ (or ~/.ulis/) │
│ agents/ skills/ mcp.yaml skills.yaml │
└────────────────────────┬────────────────────────────────┘
│ gray-matter + Zod parse
▼
┌─────────────────────────────────────────────────────────┐
│ Canonical bundle │
│ ParsedAgent[] ParsedSkill[] McpConfig │
└──────────┬──────────┬──────────┬──────────┬────────────┘
│ │ │ │
generateClaude generateOpencode generateCodex generateCursor generateForgecode
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
generated/claude opencode codex cursor forgecodeEach generate* function:
- Reads the canonical bundle
- Maps canonical types (model aliases, tool groups, permission levels) to platform specifics
- Emits native files (YAML frontmatter, JSON, TOML, MDC)
Between parsing and generation the orchestrator runs validators (src/validators/):
validateCrossRefs(agents, skills, mcp)— agent → skill (warn), agent → mcp (error), agent → subagent allowlist (warn)validateCollisions(agents, skills)— duplicate agent or skill names (error)
Errors abort the build (exit code 1, no files written). Warnings print and the build proceeds.
2.1 Build configuration
config.yaml holds CLI metadata (version, name) and install defaults.
install.linkMode controls local skill installation:
copy(default): ULIS copies generated local skill directories into each selected platform config.symlink: ULIS stages eligible local skills as native-safe skill directories, then delegates installation tonpx skills@latest add <staged-dir>so the skills library owns.agents/skills/, OS-specific symlinks or junctions, and copy fallback behavior. Successful linked installs remove duplicate generated native skill copies; failed linked installs leave generated copies in place.
Linked install applies only to local skills. Agents and platform config files remain generated per platform because their native formats differ.
Platform adapter defaults are internal to ULIS. If you need platform-native output customization, place partial config files under raw/ (for example raw/opencode/opencode.json or raw/codex/config.toml). These are merged into the generated output — objects merge recursively, and raw arrays or scalars replace generated values at the same path. See Source Layout — Raw overrides for the full rules.
Capability mismatches are handled with best-effort + comments: if a target lacks native support for a field, the value is emitted as a comment in the generated file so reviewers can see it, and the build continues (no hard failure).
2.2 Presets
Presets are additional ULIS source trees merged into the build before the selected base source (./.ulis/, ~/.ulis/, or --source). Each preset name resolves to a directory: ~/.ulis/presets/<name>/ is tried first, then bundled presets adjacent to the CLI package. User and bundled trees share the same on-disk layout as a normal source; optional preset.yaml carries display metadata only (see Field Reference — Preset metadata).
Parsed preset projects are merged in CLI order (comma-separated --preset values), then the base project is merged last so the base wins on duplicate entities and conflicting config keys. The same rules apply to build, install, and the TUI (including its validate action) when presets are selected. Discovery and labeling (user vs bundled) are implemented in src/presets.ts and src/utils/resolve-presets.ts. Optional fields for preset.yaml are documented under Preset metadata.
3. Entity Model
3.1 Agent
An autonomous task executor. Defined in .ulis/agents/{name}.md with YAML frontmatter + a Markdown prompt body.
# .ulis/agents/builder.md
---
description: Implements features from specs
model: sonnet
tools:
read: true
write: true
edit: true
bash: true
contextHints:
maxInputTokens: 80000
priority: high
toolPolicy:
requireConfirmation:
- Write
security:
blockedCommands:
- git push --force
rateLimit:
perHour: 20
platforms:
claude:
permissionMode: default
opencode:
mode: subagent
---
You are a focused implementation agent. Read specs carefully before writing code.Key fields:
| Field | Purpose |
|---|---|
model | Canonical alias: opus, sonnet, haiku, inherit. Mapped per-platform. |
tools | Permission groups: read, write, edit, bash, search, browser, agent. |
contextHints | Advisory window hints. Emitted as comments (no native equivalent on any current target). |
toolPolicy | prefer/avoid → comments. requireConfirmation → native permission controls where supported. |
security | permissionLevel: readonly → Claude plan mode + OpenCode deny perms. blockedCommands → Claude PreToolUse hooks. rateLimit → OpenCode rate_limit_per_hour. |
platforms | Per-target overrides. Applied last; they win over derived values from canonical fields. |
3.2 Skill
A composable, invocable capability. Defined as a directory .ulis/skills/{name}/SKILL.md. Both the prompt and associated files (scripts, templates) live in the same directory.
# .ulis/skills/code-quality/SKILL.md
---
description: Run code quality checks on the current file
argumentHint: "[file-path]"
tools:
read: true
bash: true
isolation: fork
---
Run the following checks...Skills become:
- Claude: slash commands in
generated/claude/commands/ - OpenCode: skill directories in
generated/opencode/skills/ - Codex: skill directories in
generated/codex/skills/ - Cursor: skill directories in
generated/cursor/skills/ - ForgeCode: skill directories in
generated/forgecode/.forge/skills/
3.3 MCP Server
Defined once in .ulis/mcp.yaml (JSON is also accepted for backwards compatibility). Each server may declare a targets list to restrict it to specific platforms.
Semantics:
- Omitted
targets— server applies to every platform (the default). - Populated array (e.g.
["opencode"]) — server applies only to the listed platforms. - Empty array
[]— server is disabled (applies to no platforms).
{
"servers": {
"github": {
"type": "local",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
},
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"localFallback": {
"command": "npx",
"args": ["-y", "@context7/mcp-server"]
}
},
"linear": {
"type": "remote",
"url": "https://mcp.linear.app/mcp",
"targets": ["opencode"]
}
}
}localFallback is used for Codex, which only supports local command-based MCP servers.
Environment variables use ${VAR} syntax everywhere. The build translates to platform-specific syntax (OpenCode headers use {env:VAR}).
3.4 Skill / Extension registry entries
Declarative installs are split into two files:
.ulis/skills.yaml — external skills installed via npx skills@latest add, keyed by platform or the "*" wildcard:
"*":
skills:
- name: mattpocock/skills/productivity/grill-me
- name: vercel-labs/agent-skills
args: ["--skill", "find-skills"]
claude:
skills:
- name: anthropics/skills
args: ["--skill", "mcp-builder"]
opencode:
skills:
- name: some-opencode-skillKey semantics:
| File | Key | Effect during ulis install |
|---|---|---|
skills.yaml | "*" | skills are installed for all platforms via npx skills@latest add -a <each-agent> |
skills.yaml | "<platform>" | skills are installed for that platform only (-a <agent-name>) |
extensions.yaml | "*" | each extensions entry runs once via the resolved runner (e.g. bunx <name> <args>) |
extensions.yaml | "<platform>" | runs the entry only when that platform is part of the install target set |
External skills are installed by npx skills@latest add, which writes into the selected project or global agent config directories. Local linked skills use the same installer after ULIS stages native-safe generated skill directories.
Each skills entry supports:
| Field | Required | Description |
|---|---|---|
name | yes | Package name, owner/repo/skill, or full URL |
args | no | Additional CLI arguments forwarded verbatim to npx skills@latest add |
.ulis/extensions.yaml — third-party CLI extensions invoked through a package runner. Useful for self-installing packages (e.g. bunx codex-supermemory@latest install) that wire themselves into a target tool's config files:
codex:
extensions:
- key: supermemory
name: codex-supermemory@latest
args: ["install"]
claude:
extensions:
- name: some-claude-helper@1.2.3
args: ["setup", "--yes"]Each extensions entry supports:
| Field | Required | Description |
|---|---|---|
name | yes | Package spec (e.g. codex-supermemory@latest); passed verbatim to the runner |
args | no | Arguments appended after name in the runner invocation |
key | no | Friendly identifier used in ulis install log lines |
Runner resolution (precedence): --runner CLI flag → runner field in config.yaml → auto-detect (bunx if available on PATH, else npx).
Extensions run last in the install pipeline (build → files → skills → extensions) because most self-installing extensions mutate the very files ulis just deployed. Each entry runs every time ulis install runs (no caching in v1). Failures log a warning and the install continues.
3.5 Hook
Part of the AgentFrontmatterSchema (hooks field). Three event types:
PreToolUse— runs before a tool call (optionally filtered bymatcher)PostToolUse— runs after a tool callStop— runs when the session ends
Hooks are native to Claude Code only. On other targets they are silently dropped (the agent still works; hooks just don't fire).
security.blockedCommands synthesizes PreToolUse hook entries automatically for Claude.
4. Capability Matrix
| Feature | Claude Code | OpenCode | Codex | Cursor | ForgeCode |
|---|---|---|---|---|---|
| Native agents | ✓ | ✓ | ✓ | ✓ | ✓ |
| Native skills/commands | ✓ | ✓ | ✓ | ✓ | ✓ |
| Hooks (PreToolUse/PostToolUse/Stop) | ✓ | — | — | — | — |
| Subagent spawning | ✓ | ✓ | comment | — | — |
| Background execution | ✓ | — | — | — | — |
| Git worktree isolation | ✓ | — | — | — | — |
| Local MCP servers | ✓ | ✓ | ✓ | ✓ | ✓ |
| Remote MCP servers | ✓ | ✓ | localFallback | ✓ | ✓ |
| Fine-grained tool permissions | ✓ | ✓ | — | — | tools list |
contextHints enforcement | comment | comment | comment | comment | comment |
toolPolicy.avoid | disallowedTools | comment | comment | comment | comment |
toolPolicy.requireConfirmation | permissionMode | permission.edit/bash | comment | comment | comment |
security.permissionLevel: readonly | plan mode | deny perms | comment | comment | comment |
security.blockedCommands | PreToolUse hook | comment | comment | comment | comment |
security.rateLimit | comment | rate_limit_per_hour | comment | comment | comment |
Legend: ✓ native · comment = emitted as comment in output file · — = not emitted
5. Build Pipeline
ulis build # all targets
ulis build --target claude # single target
ulis build --target claude,cursor
ulis install --yes # build + deploy (project mode)
ulis install --global --yes # build + deploy from ~/.ulis/Internal flow (src/build.ts): sourceDir is resolved per invocation (see src/utils/resolve-source.ts).
const agents = parseAgents(join(sourceDir, "agents"));
const skills = parseSkills(join(sourceDir, "skills"));
const mcp = loadMcp(sourceDir);
generateClaude(agents, skills, mcp, sourceDir, join(generatedDir, "claude"));
generateOpencode(agents, skills, mcp, sourceDir, join(generatedDir, "opencode"));
generateCodex(agents, skills, mcp, sourceDir, join(generatedDir, "codex"));
generateCursor(agents, skills, mcp, sourceDir, join(generatedDir, "cursor"));
generateForgecode(agents, skills, mcp, sourceDir, join(generatedDir, "forgecode"));Parsing validates against Zod schemas and fails fast with a descriptive error if a field is invalid.
6. Versioning
ULIS_VERSION = "1.0.0" is defined in src/schema.ts. This is the specification version, not the npm package version.
Migration policy:
- Patch (1.0.x): bug fixes, no schema changes.
- Minor (1.x.0): additive schema changes (new optional fields). Old configs continue to parse.
- Major (x.0.0): breaking schema changes. A migration guide will be provided.
7. Extending ULIS — Adding a New Adapter
Create
src/generators/{target}.ts:typescriptexport function generate{Target}( agents: readonly ParsedAgent[], skills: readonly ParsedSkill[], mcp: McpConfig, // ...other inputs... outDir: string, ): void { /* ... */ }Register it in
src/index.ts:typescriptimport { generateTarget } from "./generators/target.js"; // ... case "target": generateTarget(agents, skills, mcp, outDir); break;Add scripts to
package.json:json"build:target": "tsx src/index.ts --target target"Add
"target"toMcpServerSchema.targetsvalues if needed.
For capability mismatches, use buildPolicyCommentBlock(agent.frontmatter, "md" | "toml" | "mdc") from src/utils/policy-comments.ts to emit unsupported fields as comments.
8. Examples
Source: .ulis/agents/ — canonical agent definitions Generated: .ulis/generated/claude/agents/, .ulis/generated/opencode/opencode.json, etc.
Run ulis build --source example from a checkout of this repo (or bun run dev) to see a full end-to-end example.
Field reference: REFERENCE.md — auto-generated from Zod schemas.
9. Tooling
End-user CLI (see CLI.md for the full surface):
| Command | Purpose |
|---|---|
ulis init [--global] | Scaffold .ulis/ (or ~/.ulis/) |
ulis build [--target ...] | Generate configs under <source>/generated/ |
ulis install [--global] [--yes] | Build and deploy to platform config dirs |
ulis tui | Interactive dashboard for source workflows |
Repo dev scripts:
| Script | Purpose |
|---|---|
bun run build | Bundle dist/cli.js + regenerate dist/schemas/ + schemas/ (published) |
bun run dev | ulis build --source example |
bun run test | Run test suite |
bun run lint | tsc --noEmit |
bun run format | Format with oxfmt |
bun run gen:schemas | Regenerate dist/schemas/*.schema.json and schemas/*.schema.json |
bun run gen:reference | Regenerate docs/REFERENCE.md |
bun run clean | Delete dist/ and schemas/ |