Skip to content

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 settings

ulis 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        forgecode

Each generate* function:

  1. Reads the canonical bundle
  2. Maps canonical types (model aliases, tool groups, permission levels) to platform specifics
  3. 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 to npx 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.

yaml
# .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:

FieldPurpose
modelCanonical alias: opus, sonnet, haiku, inherit. Mapped per-platform.
toolsPermission groups: read, write, edit, bash, search, browser, agent.
contextHintsAdvisory window hints. Emitted as comments (no native equivalent on any current target).
toolPolicyprefer/avoid → comments. requireConfirmation → native permission controls where supported.
securitypermissionLevel: readonly → Claude plan mode + OpenCode deny perms. blockedCommands → Claude PreToolUse hooks. rateLimit → OpenCode rate_limit_per_hour.
platformsPer-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.

yaml
# .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).
json
{
  "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:

yaml
"*":
  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-skill

Key semantics:

FileKeyEffect 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:

FieldRequiredDescription
nameyesPackage name, owner/repo/skill, or full URL
argsnoAdditional 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:

yaml
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:

FieldRequiredDescription
nameyesPackage spec (e.g. codex-supermemory@latest); passed verbatim to the runner
argsnoArguments appended after name in the runner invocation
keynoFriendly 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 by matcher)
  • PostToolUse — runs after a tool call
  • Stop — 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

FeatureClaude CodeOpenCodeCodexCursorForgeCode
Native agents
Native skills/commands
Hooks (PreToolUse/PostToolUse/Stop)
Subagent spawningcomment
Background execution
Git worktree isolation
Local MCP servers
Remote MCP serverslocalFallback
Fine-grained tool permissionstools list
contextHints enforcementcommentcommentcommentcommentcomment
toolPolicy.avoiddisallowedToolscommentcommentcommentcomment
toolPolicy.requireConfirmationpermissionModepermission.edit/bashcommentcommentcomment
security.permissionLevel: readonlyplan modedeny permscommentcommentcomment
security.blockedCommandsPreToolUse hookcommentcommentcommentcomment
security.rateLimitcommentrate_limit_per_hourcommentcommentcomment

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).

typescript
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

  1. Create src/generators/{target}.ts:

    typescript
    export function generate{Target}(
      agents: readonly ParsedAgent[],
      skills: readonly ParsedSkill[],
      mcp: McpConfig,
      // ...other inputs...
      outDir: string,
    ): void { /* ... */ }
  2. Register it in src/index.ts:

    typescript
    import { generateTarget } from "./generators/target.js";
    // ...
    case "target":
      generateTarget(agents, skills, mcp, outDir);
      break;
  3. Add scripts to package.json:

    json
    "build:target": "tsx src/index.ts --target target"
  4. Add "target" to McpServerSchema.targets values 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):

CommandPurpose
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 tuiInteractive dashboard for source workflows

Repo dev scripts:

ScriptPurpose
bun run buildBundle dist/cli.js + regenerate dist/schemas/ + schemas/ (published)
bun run devulis build --source example
bun run testRun test suite
bun run linttsc --noEmit
bun run formatFormat with oxfmt
bun run gen:schemasRegenerate dist/schemas/*.schema.json and schemas/*.schema.json
bun run gen:referenceRegenerate docs/REFERENCE.md
bun run cleanDelete dist/ and schemas/

Released under the ISC License.