How to install bundles from a repository¶
dna install is the ecosystem's front door: it takes a repository URI,
detects the DNA documents inside the fetched tree, validates each one, and
writes the valid ones into your local source — through the same
kernel.write_document path a locally-authored document takes, so every
write guard runs. This guide covers the URI grammar, the install pipeline,
how conflicts and invalid documents are handled, and the provenance record
each install leaves behind.
For a quick taste, see the CLI tour; for every flag, the generated reference.
The URI grammar¶
github:owner/repo[/subdir][@ref] # shallow clone of a public GitHub repo
local:<path> # a directory already on disk
The github: form is the same grammar Genome dependencies have
always used (dna/adapters/resolvers/github.py parses both), so a URI
that works in a spec.dependencies entry works here too:
dna install github:anthropics/skills/skills/pdf --scope market --dry-run
dna install github:anthropics/skills/skills/pdf --scope market
dna install local:~/checkouts/some-skills --scope playground
subdirnarrows the install to one subtree of the repo — point it at a single bundle (.../skills/pdf) or a whole collection (.../skills).refis a branch or tag, passed to the shallow clone. Provenance is always pinned to the resolved commit, never the moving ref.local:needs no network at all — ideal for testing a drop before you publish it, or for air-gapped machines working from checkouts.
What the pipeline does¶
- Fetch —
github:URIs shallow-clone via the sameGitHubResolverthe composition engine uses;local:just resolves the directory. - Scan — the fetched tree is walked with the kernel's registered
readers (the exact same detection that loads your own scopes): a
directory with a
SKILL.mdbecomes a Skill, aSOUL.mda Soul, and so on; standalone*.yamlfiles withapiVersion+kind+metadata.nameare collected as documents too. Dot-directories (.git,.github) are skipped. - Build the plan — every detected document is validated (below) and checked
against the target scope for conflicts.
--dry-runprints this plan and stops; nothing is written. - Write — valid documents go through
kernel.write_document, one by one. A document the kernel's ownpre_saveveto guards reject is reported and skipped; the install continues with the rest. - Record provenance —
<scope>/installed.lockis upserted (see below).
The target scope comes from --scope, or is derived from the URI
(<owner>-<repo> for github:, the directory name for local:). A scope
that does not exist yet is created with a minimal Genome of its own.
Untrusted input — validation is the first defense¶
A manifest is executable behavior, so a third-party manifest is an
injection vector — that is the core of the
threat model.
dna install therefore rejects, before any write:
- documents whose
(apiVersion, kind)is not registered in your kernel — nothing is guessed or coerced; - documents whose
specfails the Kind's JSON Schema (the reported reason names the failing field); - documents whose
metadata.nameis not a plain slug — path-shaped names (../evil) never reach the filesystem layout; - root Kinds (
Genome) found in the fetched tree — an install adds content to your scope; it never lets a remote repo redefine the scope's identity.
Rejections are per-document and didactic: the plan shows each one with its
reason, the valid documents still install, and the exit code is non-zero
only when nothing usable landed. The kernel's pre_save veto guards run
on every write as the second layer — exactly as they would for a local
edit.
Conflicts¶
A document that already exists in the target scope is skipped with a
warning by default — re-running an install is idempotent. Pass --force
to overwrite existing documents with the fetched versions.
Provenance — installed.lock¶
Every successful install upserts <scope>/installed.lock, reusing the
kernel's lockfile v3 shape (dna.kernel.lock) so the same tooling that
verifies scope lockfiles can read it:
# Generated by dna install — DO NOT EDIT
lockVersion: 3
generated_at: '2026-07-10T05:02:11+00:00'
scope: market
documents:
- name: pdf
kind: Skill
apiVersion: agentskills.io/v1
origin: github:anthropics/skills/skills/pdf@9d2f1ae18723… # pinned commit
path: .
sha256: 4f0b0f2b… # canonical raw-doc digest
origin is pinned to the commit that was actually fetched (even when the
URI said @main — or nothing), path is where the document lived inside
the fetched tree, and sha256 digests the installed raw document, so you
can always answer what came from where, at which revision, and has it
changed since. Entries merge by (kind, name): reinstalls and installs
from additional sources update their own entries and leave the rest.
Offline behavior¶
No network is not an error state: github: fetches fail with a didactic
message (check the URI, the repo, your connectivity — or use local:
against a checkout), never a traceback. The test suite's github: path is
gated behind a requires_network marker and skips under DNA_OFFLINE=1,
which is how CI runs it.