The catalogue
The catalogue is the single document a tool name resolves against: one signed JSON file, format 3, served from the index. It is not a product listing and not a registry mirror — it is a security policy, and the two words are worth keeping straight. The catalogue is the signed file kapsl fetches; the Index is the whole of index.kapsl.sh — that catalogue, the vulnerability documents scans use, and the site that presents them.
What it is
latest.json the pointer: version, the payload's sha256, signed_at, url
latest.json.sig the signature — base64 ECDSA P-256, what the client verifies
payload/<year>.<week>.<n>.json the catalogue itself, immutable, cached for a year
The pointer and the payload are separate because one of them is mutable and one is not: latest.json moves on every publish, and each payload/…json is written once and never touched again (served with max-age=31536000, immutable). Verification is all-or-nothing: the public key is compiled into the binary — a key fetched from the host whose data it authenticates would prove nothing — the signature is checked over the pointer's exact bytes before a single field is read, and the pointer's sha256 then covers the payload. A stale pointer is also watched: a warning, never a refusal, when the signature is older than 30 days, because a quiet publishing fortnight must not stop the tool working but a stuck publisher should be visible.
The payload carries no versions, no digests, no dates. The schema has no temporal axis; that is the format's defining property, not an omission. The catalogue answers "what may this name do"; "which version is this, and when was it built" is answered by the index site and the registry, and by kapsl --info on your machine. Always current is why the split exists.
Scale, as of catalogue 2026.35.13 (the version these docs were checked against): 59 published projects, 241 tools, 90 scanned images, with a further 30 projects declared but not yet published (ansible, awscli, azure-cli, claude-code, helm, jupyter, poetry, ruff, typescript, openssl, and others — declared means reviewed in the index repo, built on publish). The index site shows the live number; this line is dated so its staleness is visible in the text.
What a tool entry is
Not a name→image mapping. An entry is the tool's declared boundary — everything the run is allowed to grant it, and nothing else. The wire keys:
| Key | Meaning |
|---|---|
bin | the binary to exec, relative to the image's prefix |
d | the one-line synopsis --search and --info show |
cap | capabilities granted automatically — net, rw, … and the CWD postures ro / nomount (at most one of nomount/ro/rw per tool) |
sec | the seccomp network class (net-denied / net-connect / net-listen); the live payload is 100% default, which --info reports as "no class declared" — security model |
req | hard runtime dependencies, walked transitively — flake8 requires python, so kapsl flake8 mounts Python under it automatically |
def | convenience dependencies composed in for the primary tool only |
env | variables set inside the container, {prefix}-expanded |
pass | host variable names forwarded in, explicitly |
mounts | host paths mounted in, each {path, kind, readonly} — the only way a tool reaches your machine outside the CWD |
args | per-subcommand argv defaults (appended for that subcommand) |
ports | per-subcommand port publishes (always is the exception that has to justify itself) |
sub | per-subcommand overrides of the above — replace, not merge |
Two real entries, verbatim from payload 2026.35.13:
{"bin":"bin/flake8","cap":["ro"],"d":"Check Python source for logic errors, PEP 8 style and complexity","env":{"PYTHONPATH":"{prefix}/lib/python3.14/site-packages"},"pass":["PYFLAKES_BUILTINS"],"req":["python"],"sec":"default"}{"bin":"bin/ssh","cap":["net","ro"],"d":"OpenSSH remote login client","def":["bash"],"mounts":[{"kind":"dir","path":"~/.ssh","readonly":true},{"kind":"file","path":"~/.ssh/known_hosts","readonly":false}],"sec":"default"}
The flake8 entry is the whole security story in one line: a read-only view of the working directory, no network, no mounts — a linter that cannot phone home or read your dotfiles, so an editor subprocess that runs it cannot either (IDEs and linters).
The ssh entry is the other half: the one tool you want reaching your keys. Note the split in the mounts — the key directory ~/.ssh read-only, known_hosts writable. ssh reads your identities and never modifies them, but it must record new host keys; that is the difference between a working git push and one that writes to a scratch file. The declaration is reviewed at build time and travels in the signed document, so the grant is auditable before it is ever exercised:
kapsl --info ssh
▍ ■ INFO ssh
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
OpenSSH remote login client
IMAGE ghcr.io/kapsl-sh/openssh-ssh:latest
digest sha256:1e91…2817 11 MB · 1 layers
LINES RESOLVES TO
▸ @latest 10.2p1 in use
lines float — kapsl carries security updates onto them
pin one with ssh@<version> · a digest never moves
CAPS net ro
SECCOMP net-listen (no class declared) (tier: default)
SOURCE https://www.openssh.com/portable.html
────────────────────────────────────────────────────────────────────────────────
RUN kapsl ssh SHIM kapsl -i ssh
Per-tool granularity is the only granularity that can be correct: one image ships many tools (coreutils ships ninety-six of them), so an image-wide answer could only say "something in here writes", which is not a boundary anything can enforce.
Package rules
The catalogue also carries package_rules — the per-package runtime grants for packages you install with -e, which no image declaration can know in advance. The full current set, nine rules:
| Provider | Package | Granted |
|---|---|---|
| pip | huggingface_hub | ~/.cache/huggingface directory mount + HF_TOKEN, UV_NO_HF_TOKEN forwarded |
| pip | requests | net |
| pip | httpx | net |
| pip | aiohttp | net |
| pip | flask | net |
| pip | django | net |
| pip | fastapi | net |
| npm | express | net |
| npm | @nestjs/core | net |
Two properties. The grant is keyed on (provider, package) — the narrowest place a credential can be declared: HF_TOKEN crosses the boundary only for a user who installed the thing that wants it, and it arrives with its opt-out, UV_NO_HF_TOKEN. And dotfiles a rule may mount are confined to ~/.cache/ and ~/.config/ — the client rejects anything else, so a rule cannot reach into the home directory. The caveat: a rule that matches nothing fires silently — a misspelled package or provider name is accepted, never errors, and simply never grants. Check a new rule by running the tool, not by reading it. Package providers covers the mechanism and project-scoped rules.
The index site
The catalogue is the policy; the index site is the evidence. It presents the same store the pipeline writes, and the docs never duplicate its data — anything about what tools exist, what versions they are, or what they carry, links there or to --info instead of restating it:
- Per-project pages — the project's release lines, and per-tag findings for each image and architecture, with the published scan applied. A clean scan and an unscanned image never look alike: every artifact carries its scan state.
- The assessed table — findings matched by a VEX assessment are shown as assessed, with the rationale from the document and a link to it. An assessment is a claim with a measured predicate, re-evaluated on every build; a stale one produces a finding, not a suppression.
- The raw documents — every artifact's four pieces of evidence are published as-is:
/sbom/(the CycloneDX SBOM),/scan/(the scan report),/vex/(the OpenVEX assessment),/attestation/(the SLSA provenance receipt). What Scanned before it runs describes as "the index already verified it", you can open and check.
Using it
All of this is reachable without leaving the terminal:
kapsl --search json
TOOL IMAGE DESCRIPTION
------------------ --------------------------------------------- ------------------------------
jq ghcr.io/kapsl-sh/jq:latest Command-line JSON processor
--search matches names and descriptions locally against the cached catalogue, ranked, capped at 50 rows (search_limit); your own [tools] entries are listed first and never capped.
--info prints the resolved entry — synopsis, image and digest, the version lines and what each resolves to, the declared capabilities and seccomp class, and the source URL. It is the per-tool answer to "what is this tool allowed to do", and it is what the index project page shows to everyone else.
man prints the tool's real upstream man page, not a capsule written about it:
kapsl man ls
▍ ■ MAN ls(1)
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
LS(1) User Commands LS(1)
NAME
ls - list directory contents
SYNOPSIS
ls [OPTION]... [FILE]...
DESCRIPTION
List information about the FILEs (the current directory by default).
--update refreshes the catalogue now — one conditional GET of the pointer, a payload download only if the version moved:
kapsl --update
▍ ■ SUCCESS tool catalogue already up to date · v2026.35.16
The background check does the same thing on schedule (Always current); --update is the explicit form, and it is the repair path the unknown-tool error points at.