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:

FlagDescription
exit 0Clean success (benign per-target skips allowed — they appear in the payload's skipped array)
exit 1 + stdout payloadPartial failure — the operation completed for some targets; failures are recorded inside the payload
exit 1 + stderr errorHard 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.

Note: One exception to the single-document rule: 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.

FlagDescription
E_NOT_AUTHENTICATEDNo valid login — run localskills login (also covers HTTP 401)
E_INTERACTION_REQUIREDA prompt was reached in -o json mode; details.flag names the flag that supplies the answer
E_INVALID_ARGUMENTBad flag value or combination; details.flag names the offender
E_NOT_FOUNDSkill, installed skill, folder, path, profile, MCP ref, team, or content not found (see details.kind/ref) — also HTTP 404 responses (details.httpStatus)
E_ALREADY_EXISTSResource already exists (e.g. profile create collision) — also HTTP 409 responses
E_AMBIGUOUSA reference matched more than one resource (MCP refs)
E_FORBIDDENAccess denied (HTTP 403, private skills)
E_APIThe server rejected the operation; details carries httpStatus and serverMessage
E_NETWORKTransport failure — offline, DNS, TLS, or a failed download
E_PACK_FAILEDFolder packaging failed validation (size/count/SKILL.md rules)
E_UPLOAD_FAILEDStaged package upload failed
E_AUTH_DENIEDDevice-flow login denied in the browser
E_AUTH_EXPIREDDevice code expired or was already used
E_NO_TARGETSInstall ran but nothing was installed; details.skipped explains each platform
E_INTERNALUnexpected 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.

CLI JSON Output — localskills.sh docs