---
title: "CLI JSON Output"
description: "Machine-readable output for the localskills CLI: pass -o json to any command for typed payloads on stdout, structured error codes on stderr, and strict non-interactive behavior."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.localskills.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# 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,
no envelope) and stderr carries at most one error object. Spinners, progress
lines, tips, and the update-check notice are suppressed.

```sh
localskills list -o json
```

```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"
  }
]
```

```sh
localskills push ./skill -s nope -o json
# stderr, exit 1:
```

```json
{
  "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 in 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`.

> **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**

```ts
// 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**

```ts
// 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**

```ts
// publish: action says whether a new skill was created or an
// existing one got a new version (server-side slug upsert).
// --create / --update pin the outcome instead of upserting: the
// mismatch cases fail with E_ALREADY_EXISTS (details.existing),
// E_NOT_FOUND, or E_AMBIGUOUS (details.candidates lists the
// same-name skills with publicId/slug/folderId/path).
{ 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**

```ts
// 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 (profile create collision, publish --create against a taken name — details.existing names it); also HTTP 409 responses |
| E_AMBIGUOUS | A reference matched more than one resource (MCP refs, publish --update with same-name skills in several folders — details.candidates lists them) |
| 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, so parse defensively and ignore what you don't know.

Source: https://docs.localskills.sh/cli-json/index.mdx
