← Epos

Tutorial

Quick start

In about ten minutes you will write a skill, publish it to a registry, pull it back onto a machine that has never seen it, and install it with your own values filled in. Every command below was run start to finish before it was written down.

No prior knowledge of OCI or registries is assumed.

What you need

A registry is a server that stores versioned, immutable blobs of data addressed by name and tag — reviewer:1.0.0 — and by a SHA-256 digest of their bytes. Container images live in one. So does anything else that follows the same wire protocol, which is called OCI. A skill packaged by Epos is an ordinary OCI artifact, so any registry you can already push a container image to will hold it, and any OCI client can read it back.

You need three things:

Everything below happens in one empty directory. Make one and stay in it:

mkdir epos-quickstart && cd epos-quickstart

1. A throwaway store

Epos keeps every skill it packs, builds or pulls in a local store: a directory of content-addressed blobs, the same idea as the Go module cache or ~/.docker. By default it lives under ~/.epos. EPOS_HOME moves it, which is what makes this tutorial safe to try and easy to undo — put the store inside the directory you just made, and deleting that directory deletes everything the tutorial created.

export EPOS_HOME="$PWD/.epos-store" epos store path
/home/you/epos-quickstart/.epos-store/store
It is read on every command, so it has to be exported in the shell you run the rest of these steps in. Open a new terminal and you are back on ~/.epos.

2. Author the skill

A skill is a directory with a SKILL.md at its root. The file opens with a --- fenced frontmatter block of YAML — that is where the skill's name and version live, and Epos copies the whole block into the artifact so a registry can describe the skill without unpacking it. Everything below the fence is the instructions themselves.

Anything in {{ }} is a template. It is not filled in now, and it is not filled in by the registry: it travels inside the artifact exactly as written and is rendered in step 6, when the skill is installed, with the values whoever installs it supplies. This is Helm's model, and it is the reason one published skill can serve teams that want different things from it.

Create the two files:

reviewer/SKILL.md

---
name: reviewer
version: 1.0.0
description: Reviews a change and reports what to fix.
model: '{{ .Values.model }}'
---

# {{ .Values.title }}

Review {{ .Values.language }} changes for {{ .Values.global.org }}.

See `references/checklist.md` before you start.

reviewer/references/checklist.md

# Checklist

- Every exported function has a test.
- Errors are wrapped with context.
model: '{{ .Values.model }}' is quoted on purpose. Frontmatter is YAML, and YAML reads a bare { as the opening of a flow mapping, so the unquoted form is a syntax error and packing stops before it starts:
Error: parse SKILL.md frontmatter: [4:9] could not find flow map content
   1 | name: reviewer
   2 | version: 1.0.0
   3 | description: Reviews a change and reports what to fix.
>  4 | model: {{ .Values.model }}
               ^
Below the frontmatter fence the file is Markdown, not YAML, so no quoting is needed there.

3. Pack it

epos pack turns the directory into an artifact and writes it into the local store. It reads the name and version out of the frontmatter, so it needs no arguments beyond the directory.

epos pack ./reviewer
reviewer:1.0.0 sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558

The tag is on the left and the digest is on the right. The digest is a hash of the artifact's bytes, and packing is deterministic: pack the same directory on another machine, on another operating system, and you get that same a32fa1df… back. That is what makes the digest worth pinning.

epos store ls
reviewer:1.0.0

4. Publish it

Publishing is epos push — a direct copy from the local store to your registry, with epos registry login for the credential. What was withdrawn is publishing through epos-registry, the component that fronts your registry to count downloads: it would have had to redirect the upload session to the registry behind it, and oras-go refuses an upload redirect that crosses hosts. That refusal is the fix for a real credential-leak advisory, so it is not something to work around — and it does not apply to a client pointed straight at your registry, which is what epos push is. See SPEC §4.5 for the full reasoning.

Any other OCI client works too, because the artifact Epos produced is not an Epos format. It is a plain OCI artifact, so plain OCI tools handle it — which is what the rest of this section shows with oras.

Point at your registry and log in:

export REGISTRY=ghcr.io/your-github-username echo $GITHUB_TOKEN | oras login ghcr.io -u your-github-username --password-stdin
Login Succeeded

Then copy the artifact out of the local store and into the registry. Epos's store is an OCI image layout — a directory in the format the OCI specification defines for holding artifacts on disk — which is exactly what oras cp can read from:

oras cp --from-oci-layout-path "$(epos store path)" reviewer:1.0.0 "$REGISTRY/agent-skills/reviewer:1.0.0"
Copying 11b90ecf569e application/vnd.agentskills.skill.config.v1+json
Copying 4ddd04a7449c application/vnd.agentskills.skill.content.v1.tar+gzip
Copied  4ddd04a7449c application/vnd.agentskills.skill.content.v1.tar+gzip
Copied  11b90ecf569e application/vnd.agentskills.skill.config.v1+json
Copying a32fa1dfea11 reviewer
Copied  a32fa1dfea11 reviewer
Copied [oci-layout] reviewer:1.0.0 => [registry] ghcr.io/your-github-username/agent-skills/reviewer:1.0.0
Digest: sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558

Same digest as epos pack printed. Publishing copies bytes; it does not rewrite them.

The repository name is <namespace>/agent-skills/<skill> by convention: one skill per repository, so the repository name alone says which skill it holds.

The shorter oras cp --from-oci-layout <dir>:<tag> form splits its argument at the last colon, and an Epos store tag contains a colon — reviewer:1.0.0 — so it would look for a directory called …/store:reviewer. --from-oci-layout-path keeps the two apart. Addressing the artifact by digest works with the short form too: oras cp --from-oci-layout "$(epos store path)@sha256:a32fa1df…" "$REGISTRY/agent-skills/reviewer:1.0.0".

zot is a small OCI registry that needs no configuration:

$ docker run -d -p 5000:5000 ghcr.io/project-zot/zot-linux-amd64:v2.1.18
$ export REGISTRY=localhost:5000/you

It speaks HTTP rather than HTTPS, so add --to-plain-http to the oras cp above and --plain-http to the epos pull in the next step. Skip the oras login: it has no authentication. Everything else in this tutorial is unchanged — it is the same protocol either way.

5. Pull it back

Point EPOS_HOME somewhere new. That store is empty, so what happens next can only come from the registry — it is the same situation a colleague, or your CI, starts in.

export EPOS_HOME="$PWD/.epos-fresh" epos store ls
(nothing)
epos pull "$REGISTRY/agent-skills/reviewer:1.0.0"
reviewer:1.0.0 sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558
epos store ls
reviewer:1.0.0

The digest is the one that was packed and the one that was published. That is the whole point of a content-addressed artifact: you can tell, without trusting anybody, that what arrived is what left.

pull takes the skill's name from the repository — the last path segment — and tags it in the store as <name>:<version>. That short tag is what you install by.

6. Install it with values

Installing is where the {{ }} placeholders finally get filled in. Nothing rendered them earlier: the artifact in the registry still holds them verbatim, and it can be installed a hundred times with a hundred different answers.

Write the answers down:

values.yaml

title: The reviewer
language: Go
model: opus
global:
  org: Acme
epos install reviewer:1.0.0 -f values.yaml
installed into .claude/skills/reviewer
reviewer:1.0.0 sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558
cat .claude/skills/reviewer/SKILL.md
---
name: reviewer
version: 1.0.0
description: Reviews a change and reports what to fix.
model: 'opus'
---

# The reviewer

Review Go changes for Acme.

See `references/checklist.md` before you start.

Every placeholder is gone, including the one in the frontmatter, and the quotes that made it legal YAML are still there. {{ .Values.global.org }} came from the global block: values are scoped, and global is the block every part of a skill can see — which matters once a skill is composed from several others.

--set for one-off answers

--set k=v overrides the file, key by key, and dots name nested keys. It is repeatable, and it always wins over every -f file:

epos install reviewer:1.0.0 -f values.yaml --set model=haiku --set global.org=Contoso cat .claude/skills/reviewer/SKILL.md
---
name: reviewer
version: 1.0.0
description: Reviews a change and reports what to fix.
model: 'haiku'
---

# The reviewer

Review Go changes for Contoso.

See `references/checklist.md` before you start.

title and language still came from the file; model and global.org came from the flags.

What install wrote down

Two files appeared next to values.yaml. skills.json records what you asked for. skills.lock.json records the digest that answered:

{
  "lockfileVersion": 1,
  "skills": [
    {
      "name": "reviewer",
      "version": "1.0.0",
      "ref": "reviewer:1.0.0",
      "digest": "sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558",
      "basePaths": [
        ".claude/skills"
      ]
    }
  ]
}

Commit both. The store is a cache and the lock is the truth, so the next machine resolves the same bytes you did, and two checkouts on one machine can sit on two different versions at the same time.

A skill installed with a hole in it is worse than an install that stops, so a missing value is an error rather than an empty string, and nothing is written:
$ epos install reviewer:1.0.0 --set title=Reviewer
Error: render SKILL.md: it uses a value that was not supplied
epos install reads the local store and never touches the network. Run epos pull — or epos build — first.

7. ls and uninstall

epos ls prints what this directory is pinned to. It reads the lock file, not the filesystem, because the lock is what the directory actually installed:

epos ls
reviewer:1.0.0 sha256:a32fa1dfea1118829a06ab4975e38ca502cc3c3f1ccfdab909f846276f486558

epos uninstall takes the rendered skill back out and drops it from both manifests:

epos uninstall reviewer
removed .claude/skills/reviewer
epos ls
(nothing)

The store keeps its copy, so reinstalling costs nothing and needs no network:

epos store ls
reviewer:1.0.0

epos ls is the directory; epos store ls is the cache. epos store prune collects what no tag reaches, when you want the space back.

8. Clean up

Both stores were inside this directory, which is what EPOS_HOME bought you. Deleting them leaves nothing behind:

rm -rf .epos-store .epos-fresh

Then unset EPOS_HOME, or close the shell, and Epos is back on ~/.epos.

Where to go next

You packed a skill you wrote. The other way to make one is to derive it from a skill somebody else published, with a Skillfile that reads like a Dockerfile:

Skillfile

FROM ./reviewer

APPEND references/checklist.md <<EOF
- Public API changes are noted in the changelog.
EOF
epos build . -t reviewer:1.1.0
reviewer:1.1.0 sha256:347657e7fee142b72881c4977275073d59332e5010074ec023e68fec4e9dc895

Install that and the checklist has a fourth line the base never had. The base can just as well be a skill somebody else published — FROM ghcr.io/their-org/agent-skills/reviewer:1.0.0 — in which case the build pins the digest it resolved and records it on the artifact.

A built skill lands in the store like a packed one, so steps 4 to 7 above work on it unchanged. Nothing executes during a build, and the result is a pure function of the base, the Skillfile and the context — the same three inputs always give the same digest.

Every instruction, with syntax and a worked example, is in the Skillfile reference. Every command you ran above, with its flags and their defaults, is in the CLI reference.