CLI Reference
ulis <command> [options]
Running ulis with no command prints help and exits 0. An unrecognised command exits 1 with a message on stderr. All commands exit non-zero on error with a message on stderr.
--target must name at least one platform: an empty or comma-only value (--target "", --target ",") is rejected with a non-zero exit rather than building nothing.
ulis init
Scaffold a new .ulis/ source tree with empty folders, template YAML files, and schema-backed headers.
ulis init [-g | --global]| Flag | Effect |
|---|---|
-g, --global | Scaffold ~/.ulis/ instead of ./.ulis/. No .gitignore is written in this mode. |
Project mode:
- Creates
./.ulis/withconfig.yaml,mcp.yaml,permissions.yaml,skills.yaml,extensions.yaml, and emptyagents/,skills/,commands/,rules/,raw/subfolders. - Reads the project name from
./package.json(falls back to the directory name). - Appends
/.ulis/generated/to.gitignore(creating the file if missing). - Prints a hint suggesting you also gitignore
./.claude/,./.cursor/,./.codex/,./.opencode/, and./.forge/if you don't want to commit generated configs.
Fails if .ulis/ (or ~/.ulis/ in global mode) already exists.
ulis build
Parse, validate, and generate configs into <source>/generated/<platform>/ without installing anything.
ulis build [-g | --global] [--source <path>] [--target <platforms>] [--preset <names>]| Flag | Effect |
|---|---|
-g, --global | Read from ~/.ulis/ instead of ./.ulis/. |
--source <path> | Explicit source path. Takes precedence over --global. A git URL is refused here — see Remote Sources. |
--target <platforms> | Comma-separated subset of claude,codex,cursor,opencode,forgecode. Default: all. Must name at least one platform. |
--preset <names> | Apply preset(s) before the base source (comma-separated). Resolved from ~/.ulis/presets/<name>/ first, then bundled presets shipped with the CLI. A name may also be a git repository URL — see Remote Sources. |
Output is always written under <source>/generated/<platform>/. Existing contents there are cleared before each build.
Parse and validation failures print compact multi-line diagnostics before the final No files written. summary:
[agent:worker] References MCP server "db" which is not defined in mcp.yaml
source: base
file: agents/worker.md
path: /absolute/path/.ulis/agents/worker.md
field: mcpServers[]
target: all
fix: Add "db" to mcp.yaml or remove the referencesource is base or preset:<name>. target is a specific platform, all for cross-platform fields, or none for source-only config issues. TUI validate uses the same diagnostic shape.
ulis install
Run build and then deploy the generated configs onto the target platform directories.
ulis install [-g | --global] [--source <path>] [--target <platforms>]
[-y | --yes] [--skip-rebuild] [--backup] [--preset <names>]
[--runner <npx|bunx>] [--no-prune]
[--skip-extensions] [--skip-external-skills]| Flag | Effect |
|---|---|
-g, --global | Read ~/.ulis/ and write to ~/.claude/, ~/.codex/, ~/.cursor/, ~/.config/opencode/ (Windows: %USERPROFILE%\.config\opencode\), and ~/.forge/. |
--source <path> | Override source (still writes to CWD or home depending on --global). Accepts a git URL — see Remote Sources. |
--target <platforms> | Only build/install the listed platforms. |
-y, --yes | Skip the "about to overwrite" confirmation prompt — and the remote-source trust gate, for a source this run resolves itself. It does not override the --skip-rebuild refusal below. See Remote Sources. |
--skip-rebuild | Don't rebuild — install whatever is already under <source>/generated/. Refused (-y included) if that output was built from a remote source: there is no clone left to preview, so re-run with --preset <url> instead. See Remote Sources. |
--backup | Copy each existing platform dir to <dir>.backup.YYYYMMDD_HHMMSS before writing. |
--no-prune | Keep agents and local skills from the previous ULIS install; retained stale entries become unmanaged. |
--preset <names> | Same resolution as ulis build --preset (user-global directory, then bundled), or a git URL. |
--runner <npx|bunx> | Package runner used for extensions.yaml entries. npx or bunx. Overrides runner in config.yaml. Default: auto-detect (bunx if present). |
--skip-extensions | Skip running entries from extensions.yaml. Useful in CI where network installs are not desired. |
--skip-external-skills | Skip installing external skills declared in skills.yaml. Useful in CI where network installs are not desired. |
Preset resolution: Each name maps to a directory. ULIS checks ~/.ulis/presets/<name>/ first; if that folder is missing, it uses the matching bundled preset next to the CLI (dist/presets/ when installed). A preset in your home tree with the same folder name shadows the bundled one. Multiple --preset values merge left to right, then the base source (from --source, ./.ulis/, or ~/.ulis/) is applied last — the base wins on conflicts. Interactive runs prompt to continue when a name is missing; with --yes, missing presets fail immediately.
Install strategy per platform:
| Platform | Managed entries | Preserved native config |
|---|---|---|
| Claude | generated agents/ and skills/ entries by name; commands/, rules/, … | all settings.json / settings.local.json and global .claude.json values; project .mcp.json mcpServers |
| OpenCode | generated agents/core, agents/specialized, and skills/ entries by name | opencode.json mcp |
| Codex | generated agents/ and skills/ entries by name | all config.toml values; unrelated comments and order |
| Cursor | generated agents/ and skills/ entries by name | mcp.json mcpServers |
| ForgeCode | generated .forge/agents and .forge/skills entries by name; AGENTS.md | .forge/.mcp.json mcpServers, .forge.toml |
Install records generated agents, local skills, and root entries in .ulis-manifest.json at each selected platform config root. Version 1 manifests migrate without sweeping root entries. On the first manifest-aware install, ULIS adopts the current set and removes nothing. Later installs remove previously tracked paths that are no longer generated, including platform-disabled entries, while preserving every untracked entry. Agent and skill ownership is entry-granular; OpenCode root-directory ownership is directory-granular, so pruning a stale managed root directory removes everything inside it. Manifest validation for all selected platforms completes before any destination is modified. Unselected platforms are untouched. --no-prune keeps stale paths but refreshes ownership to the current set. External skills.yaml installs are not tracked.
For Codex config.toml, Claude settings.json / settings.local.json, and global .claude.json, the existing file is the base: generated values overwrite only matching paths, while absent values remain. Other native configs retain their allowlisted preservation rules. Raw fragments win through the generated output because raw is merged during build. If --backup is set, backups include the previous manifest and managed entries before pruning.
.env loading
Before it runs anything, install loads .env files into its own environment. The child processes it later spawns for extensions.yaml and skills.yaml entries inherit that environment.
Two files are read, in this order (src/install.ts, loadDotEnv):
<destBase>/.env— the destination base, i.e. the current directory for a project install or your home directory for--global.<source>/.env— the source tree being installed.
Precedence, in full:
- A variable already present in the environment always wins.
.envonly ever adds keys, never overwrites one. - Because of that,
<destBase>/.envis read first and therefore wins over<source>/.envfor the same key. - Keys added by either file are removed again when the install finishes, so an in-process install (the TUI) does not leak them into the rest of the session.
Parsing is deliberately minimal: KEY=value per line, # comments and blank lines skipped, lines with no = skipped, and one matching pair of surrounding single or double quotes stripped. There is no variable expansion, no export prefix and no multi-line values.
Remote sources: when the source was cloned from a git URL, its .env is not read at all, and the install logs Skipped the source tree's .env: a remote source's .env is never read. A cloned .env is written by whoever owns that repository; reading it would let a repository author set the variables that decide where npx/bunx find and fetch code — PATH, HOME, NODE_*, npm_*, BUN_*, GIT_*, SSH_*, proxy variables — and so hijack the very commands you approved at the trust gate. <destBase>/.env is still read, since that file is yours. The same denylist is applied defensively to a local source's .env whenever any remote preset is part of the run.
Preset-only install (ulis preset install <names...>) reads <destBase>/.env only — it has no single source tree.
ulis tui
Launch the interactive terminal dashboard. Use it to start from a workflow, review the generated plan, select presets and platforms, validate without writing files, build generated outputs, or install with an explicit destination review.
ulis tuiThe TUI starts with workflow choices: update this project, update global configs, use a custom source, or install presets only. Each choice pre-fills an editable plan; before running, you can still change source, destination, preset layers or sources, platforms, backups, pruning, latest-build behavior, and preset extension installs. Pruning is on by default and the command preview includes --no-prune when disabled.
Keyboard controls:
j/kor arrow keys move selection.Enterconfirms selections and runs actions.xorSpacetoggles checkbox-style options (destination, presets, platforms, install options).Backspacegoes back to the previous screen.qexits from non-input screens;Ctrl+Calways exits, including while editing a path.
Mouse controls:
- Click a row to select and confirm it.
- Scroll the wheel over a panel to scroll it.
The layout is responsive: at 96 columns or wider the plan screen shows actions to the left of the summary, below that they stack with actions below the summary, and below 50×16 the TUI shows a resize prompt until the terminal grows.
Bun requirement
ulis tui renders through OpenTUI, whose renderer is only available through Bun's FFI. Every other command runs on Node as usual.
- Under Bun, the TUI runs in-process.
- Under Node, the CLI locates a
bunexecutable (BUN_INSTALL,~/.bun/bin, thenPATH), launches the TUI with it in the same working directory and environment, forwardsSIGINT/SIGTERM/SIGHUP/SIGQUIT, and exits with the child's status. - If Bun is not installed,
ulis tuiprints an installation hint and exits with code 1. Useulis build,ulis install, andulis presetinstead.
Preferences (last-used source, destination, platforms, presets, and install options) persist to .ulis-tui.json in your home directory and are only read by the TUI.
ulis preset
List presets from both ~/.ulis/presets/ and the bundled preset set. User presets are preferred when the same folder name exists in both places.
ulis preset [--list]
ulis preset list
ulis preset install <names...> [-g | --global] [--target <platforms>]
[-y | --yes] [--backup] [--runner <npx|bunx>]
[--no-prune] [--skip-extensions] [--skip-external-skills]-l / --list is accepted. The default action is list. Each line shows the directory name (what you pass to --preset), a user or bundled label, optional name / description from preset.yaml, and the display title when it differs from the folder name.
ulis preset install <names...> installs selected presets without merging a project or global source. Names may be comma-separated (a,b) or repeated (a b) and are merged in the order given. A name may be a user-global or bundled preset directory, or a git repository URL, which is cloned for the run and then discarded — see Remote Sources. Generated output is temporary and is removed after install.
| Flag | Effect |
|---|---|
-g, --global | Install to home-level platform config directories instead of the current project. |
--target <platforms> | Only install the listed platforms. |
-y, --yes | Skip overwrite prompts and the remote-source trust gate; fail fast for missing presets. |
--backup | Copy existing platform dirs/configs before writing. |
--no-prune | Keep stale agents and local skills and relinquish their previous ULIS ownership. |
--runner <npx|bunx> | Package runner for preset extensions.yaml entries. npx or bunx; default: auto-detect. |
--skip-extensions | Skip preset extensions.yaml entries. Preset skills.yaml entries still run when declared. |
--skip-external-skills | Skip installing external skills from preset skills.yaml entries. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. Also an interactive no at the trust gate, which installs nothing but is a choice. |
| 1 | Source missing, validation error, declined overwrite prompt, a trust gate that cannot be asked (no terminal, no -y), I/O failure, or one or more skills.yaml/extensions.yaml commands failing. |
All errors print a single human-readable line on stderr before exiting.
A stdin that reaches end of input without an answer (ulis install < /dev/null, a detached CI job) declines rather than waiting, so an unattended run that would have needed an answer exits 1 instead of hanging.
A piped answer still answers ordinary prompts — but it must end with a newline. printf 'y\n' | ulis install is a yes; printf 'y' | ulis install is a no, because the final partial line never reaches the reader before stdin closes. It never answers the trust gate, which requires a real terminal: without one the run fails with exit 1 rather than quietly installing nothing. Pass -y to accept both up front.
Examples
Scaffold a project and install for Claude + Cursor only:
ulis init
# edit .ulis/agents/*.md, .ulis/mcp.yaml, etc.
ulis install --target claude,cursor --yesRebuild global configs after editing ~/.ulis/:
ulis install --global --yes --backupDry-run against a fixture without touching home:
ulis build --source ./exampleReinstall from an existing build without regenerating (refused if that build came from a remote source — see Remote Sources):
ulis install --skip-rebuild --yesBuild with reusable presets:
ulis preset list
ulis build --preset team-default,typescript
ulis install --preset team-default --yesInstall a config straight from a repository (prompts before running any remote command):
ulis install --source https://github.com/acme/ulis-config#main