This is an agent skillfor the DFOS CLI — canonical in Claude Code, and installable into any coding agent. Install it any way you like; every path delivers the same skill:
From the CLI — recommended; the skill is embedded in the binary, so it always matches your installed version:
dfos skill install # ./.claude/skills/dfos/SKILL.md
dfos skill install --global # ~/.claude/skills/dfos/SKILL.mdAs a Claude Code plugin — via the marketplace, with auto-updates:
/plugin marketplace add metalabel/dfos
/plugin install dfos@metalabelWith npx skills — cross-agent (Claude Code, Cursor, …):
npx skills add metalabel/dfos -a claude-codeOr copy the file with the button below and save it to.claude/skills/dfos/SKILL.md.
DFOS CLI
The dfos CLI is a local-first relay node for the DFOS protocol. It manages
identities, content chains, and credentials. Your machine is a relay: every
operation is signed and stored locally first (in ~/.dfos/relay.db), then
optionally published to remote peers. Everything works offline; remote peers
are only needed to share with others.
Mental model (read this first)
- Two chain types. An identity chain is a self-sovereign, append-only log
of signed operations that embeds its key sets; its identifier is a
did:dfos:<31-char>DID. A content chain is an append-only log over a JSON document (referenced by hash, not embedded); its identifier is a bare 31-char content ID. Both are rooted at a genesiscreateoperation. The DID that signs a content chain's genesis permanently owns it. - The proof is public; the content is access-controlled. The protocol commits to content hashes, never plaintext — it does not encrypt. Identity ops, content ops, credentials, services, and countersignatures live on a public "proof plane." Confidentiality of the actual document bytes is enforced at the application/relay layer (a relay operator can read what it stores). This is undisclosed-by-default, not end-to-end encrypted.
- Local-first. No relay or network is needed to create identities, content, or credentials. A remote peer matters only when you publish to or fetch from someone else.
Discovering the command surface
This document is the judgment layer — the mental model, the non-obvious distinctions, and common workflows. It deliberately does not enumerate every flag, because the binary is the source of truth and flags evolve. For exact, version-matched usage:
dfos --help # all commands
dfos <command> --help # flags + args for any command/subcommandEvery data command also accepts --json for machine-readable output — always
use --json when capturing values in scripts. Aliases: identity→id,
credential→cred, peer→relay.
Installation
Check first: dfos version. If missing, install (single static binary, no runtime
deps):
# macOS (Homebrew)
brew install metalabel/tap/dfos
# Linux / macOS (curl)
curl -sSL https://protocol.dfos.com/install.sh | shThe CLI self-checks for updates on each run (non-blocking, cached 24h, prints an
upgrade hint to stderr; silent in pipes/CI and on dev builds; disable with
DFOS_NO_UPDATE_CHECK=1). Upgrade with brew upgrade metalabel/tap/dfos or by
re-running the curl script.
The Docker image
ghcr.io/metalabel/dfos:latestruns a relay server (dfos serve), not the interactive CLI — use it to host a node, not to install the command.
Keeping this skill in sync with the binary
The skill is embedded in the CLI, so it always matches the installed version:
dfos skill print # emit SKILL.md to stdout
dfos skill install # write ./.claude/skills/dfos/SKILL.md
dfos skill install --global # write ~/.claude/skills/dfos/SKILL.mdCore concepts
- Identity / DID — an Ed25519-backed identity. Its DID is
did:dfos:+ a 31-char id over the alphabet2346789acdefhknrtvz(e.g.did:dfos:cnnnft9f8a2rn938d6nkz38r847v2kr). Keys live in the OS keychain (macOS Keychain / Linux secret-service) with a file fallback at~/.dfos/keys/. - Content ID — the bare 31-char identifier of a content chain, same encoding
as a DID suffix but with no prefix (e.g.
cv7n8vkvr64cctf3294h9k4eanhff8z). - CID — an IPLD CIDv1 (
bafyrei…, ~59 chars). Addresses immutable exact bytes: a specific operation (operationCID), a committed document (documentCID), or a standalone artifact. Distinct from the living content ID / DID, which name a whole chain. - Services — an identity's discovery vocabulary: a controller-signed,
full-state set of
DfosRelaylocators andContentAnchorentries carried in the identity chain. Answers "given a DID, where do I reach it and what stable content does it publish?" The namespace is open; extension types registered by specs outside the core ride it too —DfosOrigin, the domain this identity claims, andDfosAuthorizationServer, the authorize origin that speaks for this DID (where a client holding only the DID finds the sign-in server its person authenticates to, registered by INTEGRATIONS.md). - Credential — a signed grant of scoped read or write access to content, issued by the content creator to a delegate DID.
- Countersignature — a public witness attestation referencing an operation by CID (endorsement, co-authorship, solemnization). The protocol's only inter-subjective primitive.
- Resolution stack — how each invocation picks its identity and its peer:
--as <name|did>→DFOS_AS→default-identityin config, and--relay <name>→DFOS_RELAY→default-peer. First answer wins; there is no mutable "current" pointer. - Vault — a named seed: a BIP-39 mnemonic, a fingerprint, and a derivation
counter, and nothing else. New keys derive from it at mint time
(
identity create,identity update --rotate-*) alongm/1684434803'/<index>'by SLIP-0010 ed25519. Signing never consults a vault. Nothing about a vault — the word, the phrase, the seed, the fingerprint — reaches the wire, so identities minted from one seed are unlinkable to everyone but their holder.
Quick start — local-only
dfos vault create personal # seed the keys come from; prints the phrase once
dfos identity create --name alice # mint ONE key from the vault + sign genesis (no relay needed)
dfos config set default-identity alice # standing default (or pass --as alice per command)
dfos content create - <<'EOF'
{"$schema":"https://schemas.dfos.com/post/v1","format":"short-post","body":"hello world"}
EOF
dfos content list
dfos content verify <contentId> # re-verify chain integrity locallyAll data lives in ~/.dfos/relay.db. No relay needed.
Quick start — with a relay
dfos peer add prod https://relay.dfos.com # register + verify a peer
dfos identity create --name alice --peer prod # create locally AND auto-publish genesis
dfos config set default-identity alice # standing defaults
dfos config set default-peer prod
dfos whoami # identity, signing key, credentials, peer--peer on identity create auto-publishes the genesis operation after local
creation. Creating an identity does NOT select it — nothing follows "last
created". The canonical public relay is https://relay.dfos.com.
Resolution & configuration
Config file: ~/.dfos/config.toml.
default_identity = "alice"
default_peer = "prod"
default_vault = "personal"
[identities.alice]
did = "did:dfos:..."
[relays.prod]
url = "https://relay.dfos.com"
did = "did:dfos:..."
[defaults]
credential_ttl = "24h"Resolution (highest priority first, identity and peer resolved independently):
identity: --as <name|did> → DFOS_AS → default-identity in config
peer: --relay <name> → DFOS_RELAY → default-peer in configdefault-vault is the same tier for minting: the vault new keys derive from when
no --vault names one. There is no environment tier for it.
The config tier is written by dfos config set and by nothing else — with the
single exception that creating the FIRST vault on a machine with none sets
default-vault — so parallel
agents each carrying their own --as/DFOS_AS never disturb one another.
Commands that sign echo the resolved principal and the mechanism it came from to
stderr (Signing as alice (did:dfos:…) — via --as) unless --quiet; a signing
command with nothing resolvable fails and names all three mechanisms. Public
reads are anonymous and silent. There is one spelling per mechanism: --as and
--relay are the only global selectors, and a --peer is always a command's
own.
| Variable | Purpose |
|---|---|
DFOS_AS |
Identity to act as (name or did:dfos:…) |
DFOS_RELAY |
Peer to talk to (name) |
DFOS_CONFIG |
Config file path (default ~/.dfos/config.toml) |
DFOS_NO_KEYCHAIN |
Force file-based key and mnemonic storage (CI) |
DFOS_NO_UPDATE_CHECK |
Disable the background version check |
In headless/CI environments set DFOS_NO_KEYCHAIN=1 to avoid interactive
keychain prompts. DFOS_CONFIG moves a machine's dfos files together — config,
relay.db, credentials, vaults, and file-backed keys all sit beside the config
file — and a vault's mnemonic goes with them on either backend: a vault's
keychain entry is namespaced by the config directory its metadata lives in, so a
scratch profile's personal and a real profile's personal are two phrases.
Key seeds in the OS keychain are the state that does not move — they are
addressed by public key in one machine-wide namespace — so a run that must be
isolated down to its keys sets DFOS_NO_KEYCHAIN=1 as well.
Command map
The whole verb surface, one line per group. Flags and semantics are not here:
run dfos <command> --help, which is the binary itself and therefore always
current, or read CLI.md for the full reference.
Identity (dfos identity …, alias id)
create · list · show · status (compare the local chain against the
identity's relay: in-sync / behind / ahead-unpublished / diverged, and unknown
with exit 1 when no relay could answer — silence is never agreement) · keys ·
log · update · delete · restore · publish · fetch · services ·
well-known · add-key · device-pubkey · bind-domain · verify-binding ·
remove · forget
Content (dfos content …)
create · list · show · log · download · update · delete ·
publish · fetch · verify · remove
Vaults (dfos vault …) — create · import · list · show
Keys (dfos keys …) — list · show · prune · remove <key-id|public-key|account> (one named key, dry run until --yes, candidate and orphan only) · prove <code-or-uri> (present a key to a key-add ceremony: a carriage is an authority and a code, and the identity, roles, chain head and nonce all come from resolving it; mints or names a key, shows the identity and the roles being consented to, refuses a key any identity has ever proved, posts one seven-member key-possession envelope and never retries. Presenting is not adoption — the key stays a local candidate until a chain declares it)
Credentials (dfos credential …, alias cred) — grant · revoke
Sign-in — dfos login [name|did] (--host <name-or-host> to pick from an API's advertised actions, --spaces all|<id,id> to narrow the ask to particular spaces) · cached records: dfos creds list · show · rm
Peers (dfos peer …, alias relay) — add · repin · remove · list · info · gc
Auth (dfos auth …) — proof · status
API client (dfos api …) — add · list · refresh · rm · call
Config (dfos config …) — list · get · set
Inspect & attest — dfos operation show <cid> (alias op) · dfos witness <opCID> · dfos countersigs <cid>
Top-level — whoami · status · version · recover · serve · sync · api · skill
Key distinctions (the things that bite)
--credentialvs--authorization.--credential <jws>presents a read credential to download content you don't own.--authorization <jws>presents a write credential to mutate content you don't own (content update/content delete). They are not interchangeable.- A vault is a mint-time choice, never a signing one.
--vault(anddefault-vault) affect only commands that create new key material:identity createandidentity update --rotate-*. Signing resolves the identity, then uses whichever published auth key this device holds — it never asks a vault anything. Passing--vaultto a signing command does nothing. Rotation is sticky to the vault that minted the identity's current keys, sodefault-vaultcannot quietly move an identity onto a different seed;--vaultonidentity updateoverrides that. - A vault-backed mint asks the relay before it spends an index. Two machines
holding one phrase keep two counters, so both can hand out index N — one
private key under two DIDs, invisible on either chain. Before
identity createoridentity update --rotate-*stores a key or signs an operation, it asks the resolved relay's identity index whether the key its reserved index derives already proves somewhere (GET /index/v0/identities?key=, one query per reserved key) and REFUSES the whole operation on a hit (reason: mint-index-already-proved). The reserved index stays burned, which is safe — the recovery scan's gap limit walks through burned indices by design. The probe is best-effort and loud: a relay that cannot answer and one that stops answering each mint anyway behind a one-line note naming what went unasked, while zero rows mints silently. With NO relay resolved, an imported vault carries the same note and one created on this machine does not — an imported phrase already exists elsewhere, a locally generated one does not.--no-mint-probeskips the probe entirely.recoverconverges the counter over what a seed has already spent; the probe refuses the forward collision that converged counter cannot see. - Services are full-state. On
identity update,--service(repeatable) replaces the entire services set; services you don't pass are carried forward unchanged;--clear-servicesempties the set.--serviceand--clear-servicesare mutually exclusive. identity updatehas no positional name. It acts on the resolved identity, signed with a controller key. To target alice:dfos --as alice identity update …, or setdefault-identity. (The read-only identity subcommandsshow/keys/services/deletetake an optional[name|did];logandfetchrequire the<name|did>argument.)- Publishing auto-resolves the creator, not delegates.
content create --peerandcontent publishauto-publish your identity to the peer first. But a delegated writer's identity (someone updating via a write credential) must already be published to that peer — the CLI won't push it for you. syncis the bulk pull, and it is switchable.dfos synctakes each peer's whole operation log;--peer <name>narrows it to one. A[relays.<name>]entry withsync = falseis skipped by name and reason, andsync --peeron it refuses. Explicit transfers —identity fetch/content fetch/content publish— are never gated by that switch, sodfos peer add <name> <url> --no-syncregisters a peer for those alone.- A peer's
didis a pin, not a label. Commands acting through a named peer refuse when it serves a different DID than config pins, naming both. An entry with nodidis pinned on first contact, announced on stderr; `dfos peer repin
is the only thing that moves one afterwards.peer info` reports a
mismatch (exit 1) instead of refusing, and rewrites neither the pin nor the
per-peer policy flags — those are yours once the peer is registered.
remove≠delete.identity removedrops a local config name (the chain data stays in the relay);content removeis just a no-op that points you atcontent delete— local content can't be selectively un-ingested. Neither signs a protocol delete;deleteis the irreversible protocol operation (see below).keys removeis the oneremovethat really removes something: it deletes a named key's seed from the keystore (see below).- No identity command removes key material.
remove,forget, anddeleteall leave the keys in the keystore, andforgetleaves the chain in the relay too — so a forgotten identity's keys still read as declared.dfos keys listshows what this machine holds and what claims it;dfos keys prune(a dry run until--yes) removes only keys no local chain declares, which in practice is the leftovers of an interruptedidentity create. A deleted identity's keys are never orphans — deletion is not revocation, andrestoreis real — and neither the local relay's own key nor a vault mnemonic is reachable frompruneat all.dfos keys remove <key-id|public-key|account>(also a dry run until--yes) is the by-name path for one key, and it acts on two statuses:orphan, andcandidate— the key akeys proveceremony left behind, whichpruneis built never to reach because the chain that claims it is not this machine's. It refuses everything else and says why.
Common workflows
Publish content end-to-end
dfos peer add prod https://relay.dfos.com
dfos identity create --name alice --peer prod
dfos config set default-identity alice
CONTENT=$(dfos content create - --peer prod --json <<'EOF' | jq -r .contentId
{"$schema":"https://schemas.dfos.com/post/v1","format":"short-post","body":"hello world"}
EOF
)
dfos content show "$CONTENT"Content without a $schema field prints a warning (document has no $schema field (use --no-schema-warn to suppress)). Schemas are convention-based — any
URL works; common ones are https://schemas.dfos.com/post/v1 and
…/profile/v1.
Grant another identity read access
BOB_DID=$(dfos identity show bob --json | jq -r .did)
GRANT=$(dfos credential grant "$CONTENT" "$BOB_DID" --read --json)
CRED=$(echo "$GRANT" | jq -r .credential) # the JWS to hand to bob
CRED_CID=$(echo "$GRANT" | jq -r .credentialCID) # the id you revoke later
# Bob downloads by presenting the read credential:
dfos --as bob --relay prod content download "$CONTENT" --credential "$CRED"Flags: --write grants delegated write; --ttl sets lifetime (default 24h);
--scope <contentId> narrows a grant to one chain; --broad issues a wildcard
credential covering all of your content. Revoke with
dfos credential revoke "$CRED_CID" [--peer prod] — note revocation only blocks
future fetches; a party who already downloaded the content keeps their copy.
Delegated write
# Bob updates alice's content using a write credential alice granted him.
# (Bob's identity must already be published to the peer.)
dfos --as bob --relay prod content update "$CONTENT" new.json --authorization "$WRITE_CRED" --peer prodDiscovery + witness
# Anchor content under a semantic label in alice's discovery vocabulary
# (--service REPLACES the whole set, so include every entry you want to keep):
dfos --as alice identity update \
--service id=relay,type=DfosRelay,endpoint=https://relay.dfos.com \
--service id=profile,type=ContentAnchor,label=profile,anchor="$CONTENT" \
--peer prod
# A witness countersigns the content's genesis operation:
GENESIS=$(dfos content show "$CONTENT" --json | jq -r .genesisCID)
dfos --as witness --relay prod witness "$GENESIS" --relation witnessed --peer prod
dfos countersigs "$GENESIS" --peer prodA ContentAnchor's anchor is a stable target: a 31-char content ID (mutable
chain) or a bafyrei… artifact CID (immutable). --relation is an open-namespace
tag (1–64 chars: endorses, coauthors, witnessed, …).
Local-first, publish later
dfos identity create --name alice # local only
dfos --as alice content create post.json # local only
# …later…
dfos peer add prod https://relay.dfos.com
dfos identity publish alice --peer prod
dfos content publish <contentId> --peer prodCalling a DFOS-gated API
dfos api is a generic client for any host that advertises the API-AUTH OpenAPI
convention. Register it once, then call operations by name; the document says
which artifact each route needs and the CLI signs that one.
dfos api add dfos api.dfos.com # discover and cache the document
dfos api list # what is registered, and how stale
dfos api call dfos protocol.getProtocolInfo
dfos api call dfos GET /spaces/{space} --param space=nce
dfos api refresh dfos # refetch when the staleness line says soThe profile is read from the operation's security requirements: nothing for an
anonymous route, an identity proof for the identity-proof scheme alone, and a
request proof plus X-Credential when the request-proof and credential schemes
are ANDed. --profile <anon|identity|delegated> forces one. A 401 prints the
host's challenge and stops — nothing is retried under stronger auth. A 403 prints
the actions the route requires next to the actions the credential grants.
Own-data reads — profile.getOwnProfile, the memberships.* operations — need no
credential: the document offers the identity-proof scheme as an alternative on
those routes, so dfos api call signs with a held key and nothing else.
dfos login is only for delegated access — a third party spending a credential
the user issued to it.
The delegated profile spends a credential obtained by dfos login --host <name-or-host>,
which lists the actions that host advertises and asks which of them to request. The
credential is matched to the host by the host half of its api: attenuation, not by its
audience — api:<host> and api:<host>/spaces/<id> are both grants at that host, so a
credential narrowed by --spaces to particular spaces is still selected for it and the
server decides per route. dfos creds list shows what is stored.
Raw relay access & identity proofs (escape hatch)
dfos relay call GET /.well-known/dfos-relay
dfos relay call POST /proof/v1/operations --body-file ops.json --auth
dfos auth proof PUT /content/abc/blob/bafy... --body doc.json --jti # print a proof for curl--auth signs an identity proof bound to that one request — method, host, path,
and body. There is no reusable token: a proof authorizes the request it was signed
for and goes stale in about a minute, so sign one per call.
Destructive operations & key survival
Destructive commands run immediately — no prompt. identity delete,
identity restore, content delete, and key rotation sign and (with --peer)
publish the moment you run them. Identity delete suspends signing and only
identity restore may immediately follow it; content delete remains terminal.
The existing log remains for verification. Double-check the target and --peer
first.
Key custody is two separate stories — set both up in advance.
Backup is the vault. Keys minted from a vault are described completely by its
24-word phrase plus the fixed path m/1684434803'/<index>', so the phrase written
on paper is the backup. The phrase is the only copy of that seed: it exists on the
machine that holds the vault and nowhere else. Keys created with --no-vault, or
on a machine with no vault, have no phrase covering them and live only in the
keystore.
Availability is a multi-key story, and a vault does not replace it: an identity
holds up to 256 controller and 256 auth keys, and a single controller key
authorizes identity operations (1-of-N — no multisig or threshold) while auth
keys authenticate to relays. A key enters a chain carrying its own signature over
the introduction, so a second device's key is proved from the device that holds
it: the operator custodying the chain displays a key-add code, and on that device
dfos keys add <authority>/<CODE> (alias keys prove) resolves the code, shows
the identity, the roles being consented to, and the key's six-word fingerprint,
signs a key-possession envelope with the candidate key itself — the private half never
leaves that device — presents it, and waits for the human's decision on the
operator's surface. dfos identity add-key signs only for a key this machine
already holds; where no operator custodies the chain, the CLI carries no challenge
across devices. Now losing one device is not losing the identity. This must be done
before a loss, while you still hold a controller key.
identity create mints ONE key and declares it controller, auth, and assert.
Two keys off one seed in one keychain on one machine are one custody arrangement
under two names — every event that reaches one reaches the other — so the genesis
declares one key three times, which is what is actually true. Custody splits at
the first key-add: identity add-key (another key this machine already holds) or
keys add (a key presented from the device that holds it, to a ceremony someone
else custodies the chain for).
Rotation is scoped to the roles its flags name — --rotate-auth alone leaves the
displaced key still controller and assert, and the report says so — while
--rotate-controller --rotate-auth --rotate-assert retires it outright and mints
ONE replacement for all three.
Key ids and keystore accounts are content-addressed. A key_id is
key_ + the protocol's 31-character ID encoding of SHA-256(publicKeyMultibase),
so every machine holding a key computes the same handle for it with nothing
exchanged (identity add-key --id is optional and defaults to it). The keystore
stores each seed under key:<publicKeyMultibase>; accounts an earlier version
wrote as <did>#<key_id> are still read and are never rewritten.
Restoring the backup is dfos recover. After a machine is lost, the whole path
is two commands: dfos vault import restored to adopt the phrase, then
dfos recover --vault restored --peer <relay>. It rederives keys at
m/1684434803'/<index>', asks the relay's identity index which of them any
identity has ever proved (GET /index/v0/identities?key=, has-ever-proved, so
keys a rotation left behind are found too, while a membership no proof admitted
never enters the index), pulls those chains into the local
relay, writes the private keys back into the keystore, rebuilds the vault's
minted-key records, and raises the vault's derivation counter past every index it
found in use — without which the next mint would hand a recovered index to a
second identity. Only public keys go on the wire.
Four things about it are worth knowing before you run it:
- The scan stops after 20 consecutive unused indices (
--scan-depth Nchanges it). A hole shorter than that does not end the walk. - The relay that answered is named in the output, and its silence is never an
answer. A relay serving no index (501), an unreachable one, one that quits
mid-scan, and one predating the
key=filter (which would ignore the parameter and answer an unfiltered page — caught by a sentinel probe before the scan) are all loud failures, never "no keys found".--manifest-onlyis the deliberate degradation, and it banners that no scan ran. - It writes by default and is idempotent;
--dry-runpredicts the real run without writing — it fetches each found identity's chain and verifies it in memory, so its records, counter floor, andscanCompleteare the real run's, reported in the would-mood (would-install,would-recover). - What it cannot see: a derived key no identity operation ever declared is
invisible to any index, one relay's absence is not global absence, and keys
minted with
--no-vaultare not derivable from any phrase at all (dfos keys listis what shows those).
Error recovery
Common failures and the fix (relay-origin messages reach you wrapped as
local relay rejected: … / peer rejected: …):
no identity to sign with…→ pass--as <name|did>, setDFOS_AS, or rundfos config set default-identity <name|did>.no peer to talk to…→ pass--peer <name>if the command takes one, or--relay <name>for the invocation, ordfos config set default-peer <name>; register the peer first withdfos peer add <name> <url>.identity '<n>' … not found in local relay→ create it, ordfos identity fetch <did> --peer <p>.no held <role> key … on this device→ run on the device that holds the key, or present this device's own key to a key-add ceremony (dfos keys add <authority>/<CODE>), then re-fetch the chain once the operator adopts it.Warning: OS keychain not available …→ harmless; it falls back to~/.dfos/keys/. Force file storage withDFOS_NO_KEYCHAIN=1.connect to relay: …/ connection refused → checkdfos peer info <name>; start the peer (dfos serve) if it's yours.Content chain '<id>' FAILED verification→ re-fetch:dfos content fetch <id> --peer <p>.blob bytes do not match documentCID(relay 400 on upload) → recreate the content from the exact source bytes, then publish.content '<id>' not found on peer (0 operations fetched)→ wrong content ID or it wasn't published to that peer.read credential required(relay 403 on download) → you don't own it and no standing read grant exists; obtain a read credential and pass--credential.unknown identity: <did>on publish → a referenced identity (often a delegated writer) isn't on the peer; publish that identity first.signer <did> is not the chain creator — authorization credential required→ sign as the creator, or attach a write credential via--authorization.