SkillHub

Archive safety

How SkillHub treats every skill tarball as hostile — what the packaging gate rejects, the size and shape limits, and the stable rejection codes.

A skill travels as one deterministic tar.gz. That tarball is the artifact: it is what the registry stores, what an installer downloads, and what an attacker would aim at — a ../ path that writes outside the package, a symlink pointed at your SSH key, a few hundred kilobytes that inflate into a few gigabytes.

SkillHub treats every archive as hostile input. The registry runs the checks below on every upload, the CLI runs them again on every tarball it downloads, and the packer refuses to produce an archive that would fail the path rules in the first place.

The guarantee

  • Nothing is extracted until the whole archive passes. Unpacking is an in-memory operation that returns the files as bytes; only then does the CLI write them, into a fresh staging directory that is renamed into place. Not one byte from inside a rejected archive reaches your filesystem.
  • Every rejection carries a stable code. The code is machine-readable and fixed; the human-readable message alongside it is free to change. Branch on the code.
  • Size limits bind during decompression, not after. The compressed size is checked before a single byte is inflated, and the inflater itself is capped at the smaller of the total-size limit and the ratio limit — so a decompression bomb aborts mid-stream and is never held in memory in full.
  • Path rules run on the effective path. A PAX path override goes through exactly the same checks as a raw header name, and the checks are segment-wise, so traversal that is only visible after normalization (a/b/../../../x) is caught too.
  • Only regular files and directories are accepted. A symlink is rejected even when its target sits inside the package — there is no in-tree exception. Directory entries are validated and counted, but never extracted; only regular files come out.
  • The registry re-verifies what it stored. Before unpacking an upload it re-hashes the stored object and compares it against the sha256 the publisher committed to when the publish started. A mismatch is rejected, not unpacked.

What gets rejected

ClassWhat it catchesCodes
Traversal and absolute pathsA .. segment anywhere in a path, a leading /, a Windows drive letter or UNC path, a backslash separator, an empty or . segment, a path over 1024 bytes, and control, zero-width, or bidi-override characters in a name (the trick that renders evil<RLO>gpj.sh as evilhs.jpg).path-traversal, path-absolute, path-backslash, path-malformed, unsafe-unicode
Links and special entriesSymlinks and hardlinks, character and block devices, FIFOs, contiguous files, and any entry type outside regular file and directory.symlink, hardlink, special-entry
Decompression and size limitsA compressed archive over the upload limit, decompressed output over the total limit or over the expansion ratio, more entries than the count limit, and nesting past the depth limit.gz-too-large, unpacked-too-large, ratio-exceeded, too-many-entries, too-deep
Structural malformationBytes that are not gzip or not tar, a bad header checksum, a missing ustar magic, a truncated entry, a missing end-of-archive marker, non-zero bytes after that marker (a second gzip member smuggling entries past a naive reader), an archive with no entries, duplicate paths after case and Unicode folding — including a file that collides with a directory — and the GNU and PAX extensions that can rewrite an entry's path, size, or link target.not-gzip, not-tar, empty-archive, trailing-garbage, duplicate-path, gnu-extension, pax-global, pax-unsafe

The limits

LimitValue
Compressed tarball10 MiB
Total unpacked size50 MiB
Entries (files plus directories)400
Directory nesting depth6
Expansion ratio100× the compressed size

Depth counts the directories above a file: a file at the package root is depth 0, and a/b/c/notes.md is depth 3.

The two limits you can acknowledge

Two of those limits describe a package's shape, not its safety: nesting depth (too-deep) and entry count (too-many-entries). A skill that trips one is oversized, not hostile, so a publisher can acknowledge it and ship:

skillhub publish --allow-warnings

Everything else always hard-fails. Path safety, links and special entries, the PAX and GNU rules, duplicates, malformed archives, and the three decompression-bomb limits reject the archive no matter which flags are set — --allow-warnings cannot reach them, and neither can the admin-only --force, which overrides the security scan and nothing else.

Acknowledged warnings are recorded on the published version and come back in published[].warnings, so a package that shipped over a shape limit says so permanently.

Reading a rejection

Where a code surfaces depends on which side hit it:

  • Publishing. The CLI checks your directory before it packs anything, so most problems fail locally with exit 1 and a finding you can act on. An archive the registry rejects comes back as a VALIDATION error whose issue is on tarball, with the stable code leading the message.
  • Installing. A downloaded tarball that fails the gate exits 4 with code BAD_ARCHIVE, and --json carries the exact rule in error.details.archiveErrorCode.

See Common errors for the publisher-facing codes and what to do about each.

Where to go next

On this page