CLI JSON Output
Every localskills command accepts -o json (CLI 0.20.0+) for machine-readable output. Use it from scripts, CI, and agent harnesses instead of parsing human text.
The contract
With -o json, stdout carries exactly one JSON document — the command's data, with no envelope — and stderr carries at most one error object. Spinners, progress lines, tips, and the update-check notice are suppressed.
$ localskills list -o json
[
{
"publicId": "hP5jnYv6rT",
"slug": "api-design-guide",
"name": "API Design Guide",
"description": "How to design APIs",
"type": "skill",
"visibility": "private",
"tags": ["api", "design"],
"currentVersion": 3,
"currentSemver": "1.2.0",
"tenantId": "d298c335-…",
"folderId": null,
"createdBy": "7f3a91c2-…",
"updatedAt": "2026-07-10T00:00:00.000Z"
}
]$ localskills push ./skill -s nope -o json
# stderr, exit 1:
{
"error": {
"code": "E_API",
"message": "Skill not found",
"details": { "httpStatus": 404, "serverMessage": "Skill not found" }
}
}Exit codes follow one rule:
| Flag | Description |
|---|---|
exit 0 | Clean success (benign per-target skips allowed — they appear in the payload's skipped array) |
exit 1 + stdout payload | Partial failure — the operation completed for some targets; failures are recorded inside the payload |
exit 1 + stderr error | Hard failure — nothing usable happened; parse the error object |
-o json is strictly non-interactive: anything that would prompt (pickers, confirmations, secret input) either takes its answer from a flag or fails fast with E_INTERACTION_REQUIRED naming what was needed. Commands that would scan or browse interactively require their positional argument (install, uninstall, publish, share); install also requires --target plus --global or --project; profile delete requires --force; mcp install requires every member-supplied value via --var NAME=value.
login -o json (device flow) emits compact NDJSON — first a {"status":"pending","verificationUrl":…,"userCode":…} line so your caller can surface the code, then a final {"status":"authenticated","user":{…}} line after browser approval.Per-command payloads
Payloads are declared as TypeScript interfaces in the CLI source (src/lib/json-payloads.ts) and mapped field-by-field from API responses. Arrays are always present (empty, never omitted); optional fields are omitted when unknown; timestamps are ISO-8601.
Auth
// whoami
{ username, name, email,
memberships: [{ tenantId, slug, name }],
pendingInvitations: [{ id, organizationId, organizationName,
organizationSlug, expiresAt }] }
// login --token / --oidc-token
{ method: "token" | "oidc", user?, expiresAt? }
// logout
{ loggedOut: true }Read
// list (bare array)
[{ publicId, slug, name, description, type, visibility, tags,
currentVersion, currentSemver, tenantId, folderId, createdBy, updatedAt }]
// list --public (bare array)
[{ publicId, slug, name, description, type, tags,
currentVersion, currentSemver,
author: { name, username }, downloads }]
// folders list (bare array)
[{ id, parentId, name, slug, path, skillCount }]
// mcp list (bare array)
[{ publicId, slug, tenantSlug, transport, command, url, revision,
installed: { targets, updateAvailable } | null }]
// profile list (bare array)
[{ name, active, activeViaOverride, isDefault, authenticated, skillCount }]Publish
// publish — action tells you whether a new skill was created or an
// existing one got a new version (server-side slug upsert)
{ action: "created" | "updated",
skill: { publicId, slug, name, type, visibility,
currentVersion, currentSemver, tenantId, folderId },
url, format, fileCount?,
ignoredFlags: ["folder" | "visibility"], // upsert kept server values
warnings: [] }
// push
{ skillRef,
version: { id, skillId, version, semver, message,
format?, fileCount?, createdAt } }
// share
{ skill: { publicId, slug, name },
url, installCommand,
identity: { username, tenantSlug, anonymous } }Local effects
// install
{ skill: { publicId, slug, name, type, visibility,
currentVersion, currentSemver },
version, semver, requestedRange, format, installName,
installed: [{ platform, scope, method, path, projectDir?,
format, installedAt }],
skipped: [{ platform, reason }],
requiredMcpServers: [{ tenantSlug, slug, installed }] }
// uninstall
{ skill: { installName, publicId },
removed: [installation…],
failed: [{ platform, path, error }],
purged }
// pull
{ skills: [{ publicId, slug,
outcome: "updated" | "upToDate" | "repaired" | "partial" | "failed",
fromVersion?, toVersion?, toSemver?, warnings }],
summary: { updated, upToDate, failed } }
// move
{ skill: { id, slug, name, tenantId },
from: { folderId }, to: { folderId, path }, moved }
// folders create
{ path, created: [paths…], alreadyExisted }
// profile create / switch / delete
{ name, created: true } / { name, switched: true } / { name, deleted: true }
// mcp install
{ server: { publicId, slug, tenantSlug, revision },
installed: [{ platform, scope, path, key, projectDir?, installedAt }],
skipped: [{ platform, reason }] }
// mcp uninstall // mcp pull
{ removed, warnings } { updated, upToDate, warnings }Error codes
Errors are {"error": {"code", "message", "details?"}} on stderr with a non-zero exit. The code set is closed; details carries specificity such as kind (skill | installedSkill | folder | path | profile | mcpServer | content | team), ref, flag, or httpStatus.
| Flag | Description |
|---|---|
E_NOT_AUTHENTICATED | No valid login — run localskills login (also covers HTTP 401) |
E_INTERACTION_REQUIRED | A prompt was reached in -o json mode; details.flag names the flag that supplies the answer |
E_INVALID_ARGUMENT | Bad flag value or combination; details.flag names the offender |
E_NOT_FOUND | Skill, installed skill, folder, path, profile, MCP ref, team, or content not found (see details.kind/ref) — also HTTP 404 responses (details.httpStatus) |
E_ALREADY_EXISTS | Resource already exists (e.g. profile create collision) — also HTTP 409 responses |
E_AMBIGUOUS | A reference matched more than one resource (MCP refs) |
E_FORBIDDEN | Access denied (HTTP 403, private skills) |
E_API | The server rejected the operation; details carries httpStatus and serverMessage |
E_NETWORK | Transport failure — offline, DNS, TLS, or a failed download |
E_PACK_FAILED | Folder packaging failed validation (size/count/SKILL.md rules) |
E_UPLOAD_FAILED | Staged package upload failed |
E_AUTH_DENIED | Device-flow login denied in the browser |
E_AUTH_EXPIRED | Device code expired or was already used |
E_NO_TARGETS | Install ran but nothing was installed; details.skipped explains each platform |
E_INTERNAL | Unexpected CLI failure (crash-level); message carries the underlying error |
Stability policy
The JSON surface evolves additively only. Removing or renaming a field, changing a type, or changing exit-code semantics requires a CLI major version. The error-code set is closed within a major version — new codes only appear in a major release. New payload fields and new details.kind values may appear in minor releases — parse defensively and ignore what you don't know.