Permissions
Who may write into a space is decided by signed capability blobs that the space owner issues, checked by every node when it indexes a Ref, and this page gives the exact rule, the two roles, the one-hop delegation limit, and what "web of trust" means today and what is only planned.

A space belongs to one key, and by default only that key can publish into it. To let someone else write, the owner signs a small statement, a capability, that names the other key, a role, and optionally a path. Every node that sees a document update checks it against the capabilities it knows about. So every node enforces the rule, and no single server decides.

This page gives the details: what a capability contains, what the two roles allow, how path scope and delegation chains work, what happens to an update that arrives without authority, and how contacts and membership relate to all of it. It ends with what is built and what is not.

How it works

Capabilities are certificates

Alex's description from the team notes: in object capabilities, authority is possession of a reference. This system spans processes and machines, so it uses signed certificates instead. They belong to the family of UCAN, SPKI/SDSI and ZCAP-LD, but are simpler and scoped to Hypermedia's needs. Each certificate says: this identity may do X on resource Y. The "do X" part is a coarse role. There is no list of fine-grained permissions.

Eric's three words keep the terms straight:

    a permission is the ability to do something;

    a capability is the signed thing that grants permissions;

    a role is what kind of capability it is, and therefore which permissions it grants.

A capability is a signed blob like every other Hypermedia record. The signer is both the issuer and the space being shared. There is no separate issuer field on the wire.

Field

Required

Meaning

type

yes

"Capability"

signer

yes

the issuer, which is always the space owner

sig, ts

yes

signature and issue time; the time is recorded but never compared with anything

delegate

yes

the principal receiving the grant

role

yes

WRITER or AGENT

path

no

the scope; empty means the whole space

label

no

a public, immutable note of at most 512 bytes

audience

no

used only by ephemeral authentication capabilities, never on stored grants

Shown as DAG-JSON, a grant from Alice to Bob for everything under /team:

{ "type": "Capability", "signer": {"/": {"bytes": "7QEh…alice"}}, "delegate": {"/": {"bytes": "7QEh…bob"}}, "role": "WRITER", "path": "/team", "label": "Team writers", "ts": 1757977200000, "sig": {"/": {"bytes": "…"}} }

There is no expiry field and no revocation record. Once published, a capability is valid forever on every node that has it. The label is public and immutable, so do not put private notes in it.

Who may issue

Only the space owner's key can sign a capability for that space. The daemon refuses to build one otherwise, and the CreateCapability RPC returns permission denied. So a writer cannot issue a WRITER grant in someone else's space. The nested delegations mentioned in the proto are a TODO. The one exception is the AGENT chain described below: any delegate can pass its authority one more hop by making another key its AGENT.

The two roles

WRITER may publish Refs, the blobs that set a document's current version, at the capability's path and at every path beneath it. That includes creating documents, forking, moving, deleting and redirecting under the scope. A WRITER scoped to the space root also counts as a collaborator for reading private content over HTTP. A WRITER scoped to a sub-path does not. Private peer sync works the other way round: a WRITER at any path receives the whole space's private blobs. Privacy discusses both asymmetries.

AGENT is full delegation of the issuer's key. It must have an empty path, and the indexer rejects an AGENT capability with a path. An AGENT may do everything the issuer can do in the issuer's own space. It may sign the issuer's profile and publish an alias profile pointing at the issuer. It inherits the issuer's direct grants in other spaces for one hop. This role links devices and browser sessions to an account, so the name is unrelated to AI agents. Seed Agents also use this role when an account lets an agent act for it.

An EDITOR role is reserved in the proto as a comment and does not exist in data. Team documents that list Owner, Admin, Follower, Member or Subscriber as roles describe designs. None of them is a capability role. Only WRITER and AGENT exist in the network.

Path scope

Scope is by path segment, always recursive. A capability at /team covers /team, /team/notes and /team/notes/2026. It does not cover /teammates, because matching splits on /. The no_recursive request flag and the is_exact response field exist in the API but do nothing. The daemon rejects no_recursive and always reports is_exact as false. The HM26 direction has dropped non-recursive grants altogether; see the end of this page.

The authorization rule

A node decides authorization when it indexes a Ref. The indexer asks "may the key that signed this Ref write at hm://<space>/<path>?". It answers with two lookups over the capabilities it has stored.

    If the signer is the space owner, yes.

    Compute the breadcrumbs of the target: hm://A/x/y gives [hm://A, hm://A/x, hm://A/x/y].

    Direct grant. Is there a capability signed by the owner, delegating to the signer, with role WRITER or AGENT, whose resource is one of the breadcrumbs? If so, yes.

    One-hop agent. Is there an AGENT capability delegating to the signer whose issuer is either the owner, or a key that itself holds a direct WRITER or AGENT grant from the owner on one of the breadcrumbs? If so, yes.

    Otherwise, no.

Those two lookups are the queries the daemon runs, with the positional parameters renamed for reading. The direct grant:

SELECT 1 FROM structural_blobs direct WHERE direct.type = 'Capability' AND direct.author = :owner AND direct.extra_attrs->>'del' = :writer AND direct.extra_attrs->>'role' IN ('WRITER', 'AGENT') AND direct.resource IN (SELECT r.id FROM resources r JOIN json_each(:breadcrumbs) each ON each.value = r.iri)

The one-hop agent rule:

SELECT 1 FROM structural_blobs agent WHERE agent.type = 'Capability' AND agent.extra_attrs->>'del' = :writer AND agent.extra_attrs->>'role' = 'AGENT' AND (agent.author = :owner OR EXISTS (SELECT 1 FROM structural_blobs parent_writer WHERE parent_writer.type = 'Capability' AND parent_writer.author = :owner AND parent_writer.extra_attrs->>'del' = agent.author AND parent_writer.extra_attrs->>'role' IN ('WRITER', 'AGENT') AND parent_writer.resource IN (SELECT r.id FROM resources r JOIN json_each(:breadcrumbs) each ON each.value = r.iri)))

The second query has no recursion. The chain is at most one AGENT hop on top of one direct grant, and an owner-issued AGENT capability counts as that direct grant. So an AGENT of the owner's AGENT, such as a session key issued by a linked device, is authorized. A third hop is silently unauthorized.

The rule never looks at timestamps. A capability issued after a Ref authorizes it retroactively, and one issued years ago is as good as one issued today.

Worked example: Alice, Bob and Bob's laptop

Alice owns hm://alice. She publishes one capability, and Bob publishes one of his own:

    Cap1: signer alice, delegate bob, path /team, role WRITER

    Cap2: signer bob, delegate bobLaptop, path empty, role AGENT

Now:

    A Ref signed by bob for hm://alice/team/notes: breadcrumbs are [hm://alice, hm://alice/team, hm://alice/team/notes]. The direct lookup finds Cap1 at hm://alice/team. Indexed.

    A Ref signed by bobLaptop for the same path: the direct lookup misses. The agent lookup finds Cap2 (delegate bobLaptop, issuer bob), and bob holds Cap1 from alice on a breadcrumb. Indexed.

    A Ref signed by bob for hm://alice/blog: no breadcrumb matches /team. Not indexed. It is stashed (next section). If Alice later grants Bob a root WRITER capability, the stashed Ref is retried and lands.

    If bobLaptop delegated a further AGENT key, bobLaptopScript, its Refs under hm://alice/team would fail. Alice never issued anything to bobLaptop, so the inner lookup has nothing to find.

    Bob asks Alice's public-only site for a private document under /team with a bearer token. Denied, because private reads need a root-scoped grant; see Privacy.

The stash

A Ref that fails the rule is kept. The bytes stay in the blob store. The interpretation is rolled back and recorded in a stash with the reason "permission denied" and the signer that was denied. When a capability naming that signer as delegate is indexed later, every stashed blob for that signer runs through the indexer again. This makes out-of-order delivery work: the Ref and the capability that names its signer may arrive in either order over the network, with the same result. The retry is keyed on the Ref's own signer only. When the missing piece is the grant one hop up an AGENT chain, such as Cap1 arriving after bobLaptop's Ref in the example above, that capability names bob, so bobLaptop's stashed Ref is not retried until something else re-indexes it.

Two more facts follow. Nothing checks authorization on Changes. A Change signed by anyone is stored, and becomes part of a document only when an authorized Ref points at it. Nothing checks comments either. Any key may attach a public comment to any document, and the capability field on comments is deprecated. Comment moderation is left to clients today.

Where the check runs

The Ref indexer is the gate. The daemon's own write RPCs (CreateRef, UpdateProfile, the contact RPCs) run the same rule up front so you get a clear error. PrepareChange runs no check, because it only returns unsigned bytes for you to sign. Blobs that arrive already signed, over the network or through POST /ipfs/<cid>, are stored on their hash alone and judged at index time. So a node does not need to trust the node it received a Ref from. It re-verifies the capability chain itself, offline, from signed blobs (Integrity). The capability field that the SDK can put on a Ref is informational. The indexer records it as a link and never uses it to decide.

Contacts, following and membership

A contact is a public address-book entry: "account A calls subject S by this name", plus two flags. A contact grants no permission. The daemon never consults a contact when deciding what a key may write or read.

The flags in subscribe are how the Seed apps model relationships: site: true means "I joined this site" and profile: true means "I follow this person". A contact with neither flag is treated as a legacy follow. To join a site, you publish such a contact. To leave, you remove the flag or the contact. The People and Members views of a site come from who has published a joining contact, and a writer who joins appears as a member with their role. Membership grants no permission. The indexer does not validate who signs a contact on whose behalf, but the daemon's own contact RPCs check the writer rule first. The daemon never turns a contact into a sync subscription. The apps do that. See Contact and Contact subscription.

Web of trust

What exists in code today:

    Capabilities: owner-signed, non-expiring, non-revocable grants of WRITER or AGENT with recursive path scope and a one-hop AGENT chain.

    Profile aliases: a key may declare "I am really account X", accepted only if X issued it an AGENT capability. See Identity.

    Contacts: public naming and follow flags with no permission effect and no signer validation.

    Peer and site trust for delivery: the server named in a space's siteUrl, and peers that authenticate as a WRITER, may receive that space's private blobs.

    Bearer identity over HTTP for reading private content on public-only nodes.

What does not exist in code: trust scores, transitive trust through contacts, friend-of-friend reads, reputation, endorsements, contact-based moderation or spam filtering, key rotation or recovery, revocation, expiry, an EDITOR or read-only role. The phrase "web of trust" does not appear in the daemon or the SDK. The code audit found a capability system of owner-issued grants, plus an unrelated public address book. No code path lets one user's trust in another change what a third party may read or write.

Working with permissions

In the Seed app

In the Seed app, a document's Collaborators view lists who can write it, with inherited grants from parent paths. The owner invites members there. Pick accounts and a role, and the app signs one capability per account, scoped to the document's path. It pushes them to the site, so they take effect as fast as a comment does. The Join and Follow buttons publish contacts.

CLI

seed-cli capability create --delegate z6MkBob… --role WRITER --path /team --label "Team writers" seed-cli capability create --delegate z6MkLaptop… --role AGENT seed-cli account capabilities hm://z6MkAlice…/team # grants that cover this path, inherited included seed-cli document create --account z6MkAlice… --path /team/notes -f notes.md # write into a space that delegated to you seed-cli contact create --subject z6MkBob… --name "Bob" seed-cli contact list z6MkAlice… --account # Alice's contacts; --subject for who names Alice

The signing key must be the space owner for capability create. With --account, the CLI looks up a WRITER or AGENT grant for the signing key and records its CID on the Ref. See Seed CLI.

SDK

In the SDK, createCapability({delegateUid, role, path?, label?}, signer) returns a publish input, and client.publish sends it. resolveCapability(client, targetAccount, signerAccount, path?) finds the grant that lets a signer write into another space. createContact, updateContact and deleteContact cover the address book. Reads go through client.request('ListCapabilities', {targetId}). See SDK.

Web API

On the Seed API, GET /api/ListCapabilities?targetId=<packed id> returns every grant whose scope covers the target, inherited ones included. The listing matches paths by string prefix, so a grant at /team also shows up for /teammates, where it authorizes nothing. GET /api/ListDocumentCollaborators?targetId=… combines grants and joining contacts into the view the app shows. GET /api/AccountContacts?__value=<uid> and GET /api/SubjectContacts?__value=<uid> list contacts by owner and by subject. Grants are published like any blob with POST /api/PublishBlobs. On the daemon, the gRPC AccessControl service has ListCapabilities, ListCapabilitiesForDelegate, CreateCapability and GetCapability. Pagination and ignore_inherited are declared but not implemented. See Web API.

Agents

A Seed Agent writes with its own key. A human who wants an agent to publish into their space grants it a capability, usually WRITER on a path. Then the agent's write to hm://<space>/<path> succeeds. An agent that owns a space can grant others with the write verb: action capability.grant (alias capability.create) with delegate, role, and optional path and label, always tried with dryRun first. The agent server verifies delegation the same way the daemon does, by checking a capability blob signed by the account for the signer. It offers no revocation, because the protocol has none. Contacts are contact.create and contact.delete. See Write and Signed API.

An external agent using the seed-cli skill should hold its own key and get a scoped WRITER capability. It should not use the human's key. Then every change carries the agent's attribution, and a human's later edits show as separate changes. See Building agents.

Where this is going

Design direction as of September 2026. None of it is code:

    Revocation. The RevokeCapability RPC is a TODO in the proto and a shaped project in the team's plans. Until it exists, the mitigation is to grant narrowly (WRITER on a path, to a key you control) and to treat a lost delegate key as permanent.

    EDITOR and read roles. Reserved in the proto. The sharing-permissions product work (August 2026) asks for read grants, share links and invitations, which the current roles cannot express.

    HM26. The node and resource redesign drops path-scoped grants in favour of invitations at the space level, discontinues the non-recursive flag, treats capabilities as a special category of resource, and wants a group concept so one grant can name several keys. Existing sub-document grants would not migrate. See Roadmap.

    Trust beyond grants. Team essays describe contextual trust rooted in communities, with no global PGP-style web. None of it is specified yet.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime