docs / automation

Automation & CI

The reading for scripts, CI jobs, and agents: how to branch on exit codes, what is inside a --json report, the action vocabulary, the full pattern for wiring skillmod into a pipeline, and the conventions for non-interactive environments. Hands-on sequences by task live in the scenario guide, and the per-command flags in the command reference.

Exit codes

Read the exit code when a script, a CI job, or an agent needs to decide something from a skillmod run instead of a person reading its output:

ExitMeaningWhen it appears
0All checked entries are consistentAn inspection or operation completed normally
1Operational or input errorAny command
2Drift detectedverify finds contents that do not match SKILL.lock
3Partial completion: independent work finished, and one or more targets were safely preservedget, sync, update, and remove keep a target instead of overwriting it

get, sync, update, and remove return 3 when they keep a target instead of overwriting it. That is not total failure and not total success: read the report to see what was preserved and why.

Drift gets its own 2 because it is a different conclusion from “something went wrong.” The job of verify is checking consistency: contents that do not match the lock record are a definite state judgment, worth separating from argument errors and environment problems so a script can handle each on its own.

Reading it as a build gate: treat any nonzero exit as a failure. 2 means the checked-out skill environment does not match the declaration, and 3 means targets were safely preserved — either one should stop the pipeline, with the report left for troubleshooting.

--json

Every command supports --json. The command prints its human summary as usual; adding --json also emits a structured report on stdout.

A report separates the command that ran from the outcome of each declaration and of each installation directory:

FieldMeaningaction values
actionThe command that produced the reportget, init, list, prune, remove, share, sync, update, upgrade, verify, why
entries[].actionThe aggregate outcome of one declarationconflict, drift, install, installed, keep, local, local-drift, matched, missing, partial, prune, remove, skip, stale, unlocked, unresolved, unverifiable, update
entries[].targetResults[].actionThe outcome of one installation directorydrift, install, installed, keep, missing, remove, skip, unlocked, unverifiable

Field names and action identifiers are never translated. Branch on the command and action identifiers, not on natural-language notes: a note is translated prose for people, and it changes with the output language.

Inspection reports (list, why, and verify) also carry requestedVersion for remote entries: it is the exact SKILL.mod value, while version is the installed version from SKILL.lock. An empty requestedVersion means the declaration tracks latest, which the lock pins.

upgrade uses the same shape with action: "upgrade" and one entry named skillmod. The entry action is keep when the running version is the requested one and update when a newer release exists, with entries[0].version naming that release; the executable's own targetResults[0].action says what happened to the file: installed means it was replaced, install means a dry run verified it, and keep means it was left alone. The report is written even when the upgrade fails, so --json always decodes one document.

$ skillmod upgrade --check --json
watch for a release without downloading it

Action vocabulary

The command level is the eleven command names themselves: whichever command produced the report is the top-level action. The full vocabulary for the entry and directory levels follows.

entries[].action (the aggregate outcome of one declaration)

actionMeaning
conflictThe target directory holds different content, settled by the conflict policy
driftContents do not match the lock record
installA planned installation that a dry run verified, not yet written
installedWritten, and matching the lock record
keepAlready as expected, left alone
localA local declaration, with no remote source
local-driftThe local entry was edited and no longer matches the recorded baseline
matchedinit matched the source of an existing lock record or verified snapshot
missingThe installation directory does not exist
partialPartial completion: some work finished, and targets were preserved
pruneprune removed a stale installation or lock record
removeThe declaration (or a share link) was removed
skipThe target was skipped by the conflict policy
staleThe declaration is gone from SKILL.mod while the installation remains; clean it with prune
unlockedThe declaration has no matching record in SKILL.lock
unresolvedThe source cannot be resolved to a Git repository and is kept as a local entry
unverifiableThe directory cannot be read or its contents cannot be verified
updateUpdated; in an upgrade report, a newer release exists

entries[].targetResults[].action (the outcome of one installation directory)

actionMeaning
driftDirectory contents do not match the lock record
installA planned installation that a dry run verified
installedWritten, and matching the lock record
keepThe directory is already as expected and was left unchanged
missingThe directory does not exist, or a share link is absent
removeThe directory (or share link) was removed
skipSkipped by the conflict policy
unlockedNo lock record to check the directory against
unverifiableThe directory cannot be read or its contents cannot be verified

The stability conventions of the report shape:

  • Branch only on the command identifier and the action values at each level: they are stable and never translated; a note is translated prose and is not something to branch on.
  • entries[].action is an aggregate that folds the entry's target results into the most actionable status; read targetResults when one directory matters more than the declaration as a whole.
  • Per-directory facts appear only in targetResults: a conflicting or unreadable directory stays visible there while the rest of the command succeeds.
  • requestedVersion appears only on remote entries of inspection reports, and an empty value means the declaration tracks latest.
  • The upgrade report is written even when the upgrade fails, so --json always decodes one document.

Wiring into CI

The full pattern is four steps: checkout, install skillmod, sync, verify. verify is the build gate — a nonzero exit fails the build.

$ git clone https://github.com/acme/project.git && cd project
checkout: SKILL.mod and SKILL.lock arrive with the repository
$ go install github.com/huija/skillmod@latest
install skillmod; when the image has no Go toolchain, use the release binaries from GitHub releases
$ skillmod sync
install or align the declared skills against the lock
$ skillmod verify
build gate: exit 2 fails the build

Wire the exit code of verify into the pipeline explicitly:

$ skillmod sync
$ skillmod verify --json > verify.json
$ code=$?
$ [ $code -eq 0 ] || { cat verify.json; exit 1; }
exit 2 (drift) or 3 (partial completion) fails the build, with the report left for troubleshooting
  • sync is idempotent: it installs or aligns declared content against SKILL.lock and does not silently overwrite local modifications; verify checks the result after alignment.
  • Add --json for a machine-readable conclusion, branch on the command and action identifiers, and keep the report as a build artifact for troubleshooting.
  • CI has no TTY: give the selection up front for any command that could open one, as described in the next section.
  • To watch for a new skillmod release in the pipeline without downloading it: skillmod upgrade --check --json.
Network: an already materialized immutable snapshot may work offline, but latest-version resolution requires remote references. A first sync in a clean environment usually needs access to the repositories.

Non-interactive conventions

CI and scripts have no TTY, so any command that could open a selection needs that selection given up front. --all and --yes divide the work: --all states the selection itself — every skill the repository publishes for get, every declared entry for remove, every installed skill for share; --yes answers the confirmations that follow a selection and never decides what the selection is.

A non-interactive run of a multi-skill repository has to say --all to get past the selection step. On a terminal, --yes still lets the user pick skills; it skips only the later confirmations. The same rule applies to share and remove.

$ skillmod get github.com/acme/agent-skills//review --all --yes
--all gets past the selection, --yes past the confirmation; neither one prompts
$ skillmod remove --all --yes
$ skillmod share --all --agent claude --yes

The use of --dry-run is to review the report before the write: init, sync --relink, remove, prune, share, and upgrade all support it. upgrade --dry-run downloads and verifies without replacing the executable, and --check stops before downloading. A dry run writes no state and creates no links, so it suits confirming before acting.

Language and environment

Command help, summaries, prompts, and errors follow SKILLMOD_LANG; when it is unset, the system locale is read from LC_ALL, LC_MESSAGES, then LANG, falling back to English when none is available or supported.

JSON field names and action identifiers are unaffected: a script can branch on the identifiers whether or not SKILLMOD_LANG is set. Setting it keeps the human-readable output in one language — worth fixing in CI logs, so prompts do not change shape with the runner.

$ SKILLMOD_LANG=zh_CN skillmod verify
human output pinned to Chinese, exit code unchanged
$ SKILLMOD_LANG=en skillmod verify --json
the --json report does not depend on the language
Further reading: the word-for-word flag vocabulary for every command is in the command reference; hands-on sequences by task are in the scenario guide; errors and odd states are in troubleshooting.