Common errors
Exit-code-driven troubleshooting — auth failures, ambiguous names, copy fallback, offline sync, and orphaned skills.
Every command exits with a code that tells you what class of problem you hit, and
--json failures return a structured error. This page maps the common situations
to their cause and fix.
Exit codes at a glance
| Code | Class | Typical cause |
|---|---|---|
0 | OK | Success. |
1 | User / validation | Bad input, ambiguous ref, invalid arguments. |
2 | Auth | Not logged in, or not authorized for the action. |
3 | Network | Offline, transport failure, retry later. |
4 | Integrity / scan | Blocking scan finding, or a content-hash mismatch. |
With --json, a failure returns { ok: false, error: { code, message, details? } }
on stdout — read message first.
Not logged in (exit 2)
Publishing, or any registry action, requires authentication. If you see an auth error, confirm who you are and log in:
skillhub whoami
skillhub loginIn CI, the browser login flow can't run. Use a token instead — either
login --token or the token environment variable. See
API tokens.
Ambiguous short name (exit 1)
A bare skill name has to resolve to exactly one skill across your configured registries. If the same name exists in more than one, the command fails as ambiguous. Fix it by using the fully qualified ref:
skillhub install @studio/design-systems/tokens-workflowSymlinks unavailable — copy fallback
Skills install once into a canonical store and fan out to each agent by
symlink. On platforms without symlink support, SkillHub automatically
falls back to copying and prints a notice — nothing fails. You can also opt
into copying everywhere with --copy.
Copied skills show up in skillhub list with copyMode: true. This is expected
behavior, not an error.
A copy in an agent skills directory is rebuilt from the store on every sync that touches the skill, so edits made there are replaced — and the run says so when the copy differed. Edit the skill in the store, where edits are protected.
Edited a skill in the store (exit 1)
Store skill directories are yours to edit, and an edit there is protected: the CLI records a content baseline at install time, so it can tell a clean store skill from one carrying your changes.
A run that would replace or delete an edited skill — a version bump on
skillhub sync, sync --prune, or skillhub remove — asks first on a
terminal. With nobody to ask (--json, or a piped stdin) it refuses, touching
nothing, with exit 1 and code STORE_MODIFIED:
2 skill(s) in the store carry local edits and would be replaced:
formatter@1.4.0 (1.5.0 incoming), linter@2.0.0. Copy your changes out of the
store to keep them, or re-run with --discard-modified.error.details.skills[] names each one with ref, name, version,
storeDir, action (overwrite or remove), and incomingVersion when a
newer version is on offer.
Two things to know:
--yesisn't consent. It answers a confirmation with its default, and this one defaults to No. Only--discard-modifieddiscards.sync --frozenchecks content too, and fails with the same code — an edited store is drift a lockfile comparison can't show, because the lockfile still matches itself. It reads the store on the machine running it, so a fresh CI checkout (no store yet) has nothing to compare; the check bites on developer machines and on cached or reused workspaces.sync --discard-modifiedputs the published content back over every edited skill, at the version the resolved state holds — including the one already installed. On the other commands the flag is permission for a replacement or deletion that run was already making.- Or keep the edit and publish it.
syncsends modified store skills back to the registry they came from — selected on a terminal, all of them with--publish— and the store then matches what was published, so the skill reads clean. See Publishing what you changed.
A skill didn't publish (PUBLISH_FAILED)
When skillhub sync or skillhub publish pushes several skills at once, one
failing doesn't end the run: every skill is attempted, each success is recorded
as it lands, and the pull half of a sync runs either way. The run then fails
with code PUBLISH_FAILED, carrying the worst exit class any single skill
raised — 2 for a permission refusal, 3 for a transport failure, 1 for
everything else.
error.details.publishes[] states what happened to each skill, and on sync
error.details.sync carries the complete sync result alongside it:
{
"ref": "@acme/kit/private-notes",
"name": "private-notes",
"status": "forbidden",
"version": null,
"tag": null,
"code": "FORBIDDEN",
"message": "You do not have permission to publish to @acme/kit."
}held-for-review is not one of these failures: the version is on the
registry and waiting for an admin, and the run exits 0. See
Review-required flow.
One case counts as a failure while still reporting a landed version: a publish
the registry accepted whose result could not be written back to the lockfile —
another skillhub process holding the scope lock, or an unwritable scope. The
outcome keeps its published/held-for-review status and version, and
carries code: "PUBLISH_UNRECORDED". That version number is spent on the
registry, so the run cannot report clean: re-run once the other process has
finished, or publish a later number with --version <semver>.
Offline sync — tolerated or exit 3
skillhub sync normally uses the network to resolve your subscription set. Run it
offline and the outcome depends on what's already on disk:
- If the lockfile is already satisfied on disk, sync warns that it's offline and
exits
0— there's nothing to reconcile, so being offline is tolerated. - If the lockfile cannot be reconciled offline — a resolve is genuinely
needed — sync changes nothing rather than guessing and exits
3. Reconnect and run it again:
skillhub syncIn CI, sync --frozen fails with exit 1 if the lockfile would drift. That's by
design — it tells you the committed lockfile is out of date, not that anything is
broken.
Orphaned skills after revoked access
If a registry is deleted, or your access to it is revoked, its skills stay on
disk but are marked orphaned in skillhub list. They're no longer backed
by a reachable subscription. Clear them out with a pruning sync:
skillhub sync --pruneDeletion always requires --prune — a plain skillhub sync keeps orphaned
skills on disk and only warns, so nothing disappears from under you by accident.
Symlinks in a skill directory (exit 1)
A skill package carries regular files and directories only, so skillhub validate
and skillhub publish reject a directory containing a symlink with
local.symlink — including one whose target sits inside the skill. The same
applies to a path carrying control characters or a backslash
(local.unsafe_path).
Replace the link with the file it points at, or leave it out of the package. The reasoning is in Archive safety.
Skill too large to publish (exit 1)
Two size ceilings apply to a published package, and tripping either is a hard error, not a warning you can acknowledge:
- 50 MiB across all files.
skillhub validatecatches this locally aslocal.too_large, before anything is packed. - 10 MiB for the packed tarball. The registry refuses an upload over that
(
gz-too-large).
Both usually mean the same thing: something is in the directory that shouldn't
ship — a checked-in binary, a model file, a node_modules. Trim it and re-run:
skillhub validateA tarball that expands to more than 100× its compressed size is rejected as
ratio-exceeded even when it clears both ceilings. In practice that's one large
generated or highly repetitive file; drop it rather than trying to compress
around the limit.
Structural warnings block a publish (exit 1)
Two limits describe a package's shape rather than its safety, so they surface as warnings you can acknowledge:
| Finding | Limit |
|---|---|
local.too_many_files | 400 entries |
local.too_deep | 6 directory levels |
skillhub validate reports them and still exits 0. skillhub publish asks you
to confirm them — but a non-interactive session (--json, or no TTY) has nobody
to ask, so unless the run carries --yes it fails with
WARNINGS_UNACKNOWLEDGED and exit 1, listing what it found, before any network
call. Acknowledge them up front instead:
skillhub publish --allow-warningsAcknowledged warnings are recorded on the published version and returned in
published[].warnings. --allow-warnings covers these two limits and nothing
more — every other archive rule hard-fails.
Drift and integrity (exit 4)
Exit 4 also covers a content-hash mismatch — the bytes on disk don't match the
version they claim to be. Run skillhub doctor to locate the drift and follow its
fix:
skillhub doctorA downloaded tarball that fails the archive gate stops at the same exit code with
code BAD_ARCHIVE, before anything is extracted. With --json, the exact rule it
broke is in error.details.archiveErrorCode — see
Archive safety for what each class of code means.