Chapter 34 - Code Collab
34
Nostr Implementation Possibility - Hex № 34

`git` stuff

Announce git repositories, send patches and pull requests, file issues, and track their status — all as signed Nostr events.

1 Overview

This NIP defines code collaboration with git over Nostr. The code itself stays on ordinary Git servers. Nostr carries everything around the code: repository announcements, branch state, patches, pull requests, issues, and status changes.

A maintainer publishes an announcement event that describes a repository. A contributor sends a patch or a pull request as an event to the relays the announcement lists. Anyone files issues the same way. Status events then mark each thread as open, applied, merged, resolved, closed, or draft.

No single forge holds the project hostage. The repository can move between Git servers. Its identity, its history of proposals, and its discussion stay on Nostr, tied to keys instead of accounts.

Hint!

Think of this NIP as a mailing-list workflow with cryptographic identities. Patches travel like email in the kernel style. Pull requests point at branches on clone servers, GitHub style. Both are plain events you can verify.

Kind map 30617 - 30618 REPOSITORY — announcement and state (addressable) 1617 - 1618 - 1619 CHANGES — patch, pull request, PR update 1621 ISSUES — bug reports, requests, questions 1630 – 1633 STATUS — open, applied/merged/resolved, closed, draft 10317 USER — grasp server list (replaceable)
Fig. 34-1 — Kind map. Ten event kinds cover the whole collaboration workflow.

2 Repository Announcements

A repository lives on one or more Git-enabled servers. A kind 30617 addressable event announces that it exists. The author of the announcement asserts that they maintain the primary project, unless the event carries a u tag that marks it as a subordinate fork.

Wire format - kind 30617 repository announcement
{
  "kind": 30617,
  "content": "",
  "tags": [
    ["d", <repo-id>],                       // usually a kebab-case short name
    ["name", <human-readable project name>],
    ["description", <brief description>],
    ["web", <url for browsing>, ...],
    ["clone", <url for git-cloning>, ...],
    ["relays", <relay-url>, ...],
    ["r", <earliest-unique-commit-id>, "euc"],
    ["maintainers", <other-recognized-maintainer>, ...],
    ["u", "30617:<pubkey>:<identifier>|<git-url-https-preferred>",
         <relay-hint>, <author-pubkey>],   // marks a subordinate fork
    ["t", <arbitrary string>]               // hashtags labelling the repo
  ]
}
Kind 30617 tags
TagValuesRule
doneThe repository identifier. The only required tag. Usually a kebab-case short name.
nameoneHuman-readable project name.
descriptiononeBrief human-readable project description.
webmanyWebpage URLs for browsing, if the Git server offers a web view.
clonemanyURLs you can give to git clone.
relaysmanyRelays this repository monitors for patches and issues.
rone, marked eucThe earliest unique commit ID. Identifies the repo among forks. See below.
maintainersmanyPublic keys of other recognized maintainers.
uoneMarks the repository as a subordinate fork. The value points to the parent: a 30617:<pubkey>:<identifier> address or a git URL, HTTPS preferred. A relay hint and the author pubkey may follow.
tmanyArbitrary hashtag strings that label the repository.

The r tag with the euc marker holds the earliest unique commit of the repo. It groups copies of the same project hosted in different places. In most cases it is the root commit. After a permanent fork between two projects, use the first commit after the fork instead.

Tech Note

The source spec writes the u tag value as one string with a | between the event address and a git URL. It does not say when to use which side, and it spells "identifer" and "prefered". The intro sentence also only says a u tag cancels the maintainer assertion; the fork semantics live in a trailing comment. Treat the exact u format as underspecified.

Nostr clone URL format

A nostr:// URL references a repository announcement. With a git-remote-nostr helper installed, git clone accepts it directly. Three forms exist:

nostr:// clone URL forms
nostr://<naddr>
nostr://<npub|nip05>/<identifier>
nostr://<npub|nip05>/<relay-hint>/<identifier>

-- examples --
nostr://npub15qy…2exs5cyejr/relay.ngit.dev/ngit
nostr://npub15qy…2exs5cyejr/my%20%F0%9F%9A%80%20repo
nostr://danconwaydev.com/ws%3A%2F%2Flocalhost%3A7334/my-local-only-repo
nostr://danconwaydev.com/relay.ngit.dev/ngit
  • <identifier> is the d tag value of the kind 30617 event.
  • <relay-hint> is a relay URL. You may drop its wss:// scheme for brevity.
  • Both <relay-hint> and <identifier> must be percent-encoded per RFC 3986 §2.1.
  • <naddr> and <npub> are NIP-19 encodings; <nip05> is a NIP-05 address.

3 Repository State

A kind 30618 addressable event is an optional source of truth for the branches and tags of a repository. Its d tag matches the identifier of the corresponding announcement.

Wire format - kind 30618 repository state
{
  "kind": 30618,
  "content": "",
  "tags": [
    ["d", <repo-id>],
    ["refs/<heads|tags>/<branch-or-tag-name>", <commit-id>],
    ["HEAD", "ref: refs/heads/<branch-name>"]
  ]
}

The refs tag may appear many times, or not at all. Each one maps a branch or tag name to a commit ID, exactly like the output of git show-ref. The HEAD tag names the default branch.

4 Patches

Anyone can send a patch or a pull request to any repository. Send them to the relays listed in the announcement's relays tag. Include an a tag that points to the announcement address. Use patches when each event stays under 60kb. Use pull requests otherwise.

A kind 1617 patch carries the output of git format-patch in its content. Patches in a set chain together: each later patch includes a NIP-10 e reply tag that points to the previous patch. The first patch of a revision includes a NIP-10 e reply tag that points to the original root patch. The first patch in a series may be a cover letter in git format-patch format.

Wire format - kind 1617 patch
{
  "kind": 1617,
  "content": <output of git format-patch>,
  "tags": [
    ["a", "30617:<base-repo-owner-pubkey>:<base-repo-id>"],
    ["r", <earliest-unique-commit-id-of-repo>],
    ["p", <repository-owner>],
    ["p", <other-user>],          // optional: bring it to their attention

    ["t", "root"],                // omitted for later patches in a series
    ["t", "root-revision"],       // only on the first patch of a revision

    // optional: keep a stable commit id when the patch is applied
    ["commit", <current-commit-id>],
    ["r", <current-commit-id>],
    ["parent-commit", <parent-commit-id>],
    ["commit-pgp-sig", "-----BEGIN PGP SIGNATURE-----..."],
    ["committer", <name>, <email>, <timestamp>, <tz offset, minutes>]
  ]
}
Kind 1617 tags
TagRule
aAddress of the target repository announcement.
rEarliest unique commit ID of the repo. Lets clients subscribe to all patches for a local repo. A second r tag may carry the current commit ID so clients find patches for a specific commit.
pThe repository owner. Extra p tags may notify other users.
troot on the first patch of a set; root-revision on the first patch of a revision. Omit on later patches in a series.
commitOptional. The commit ID the patch produces on the proposer's machine.
parent-commitOptional. The parent of that commit.
commit-pgp-sigOptional. The PGP signature of the commit. An empty string means an unsigned commit.
committerOptional. Name, email, timestamp, and timezone offset in minutes.
Hint!

The four optional commit tags exist for one reason: byte-for-byte reproducible commits. With them, the commit a maintainer creates by applying the patch gets the same ID it had on the proposer's machine. Omit them all if nobody cares.

Tech Note

The source sentence reads "The first patch revision in a patch revision SHOULD include a reply to the original root patch". The doubled phrase is a typo; the intended meaning — first patch of a revision replies to the root patch — is restated above.

5 Pull Requests and Updates

A kind 1618 pull request points to proposed changes that live on a Git server instead of inside the event. Its content is Markdown text that describes the change.

Wire format - kind 1618 pull request
{
  "kind": 1618,
  "content": <markdown text>,
  "tags": [
    ["a", "30617:<base-repo-owner-pubkey>:<base-repo-id>"],
    ["r", <earliest-unique-commit-id-of-repo>],
    ["p", <repository-owner>],
    ["p", <other-user>],              // optional

    ["subject", <PR-subject>],
    ["t", <PR-label>],                // optional, repeatable

    ["c", <current-commit-id>],       // tip of the PR branch
    ["clone", <clone-url>, ...],      // at least one url with the commit
    ["branch-name", <branch-name>],  // optional recommended name

    ["e", <root-patch-event-id>],     // optional: PR revises this patch
    ["merge-base", <commit-id>]      // optional: common ancestor
  ]
}
Kind 1618 tags
TagRule
aAddress of the target repository announcement.
rEarliest unique commit ID, for subscriptions.
pRepository owner; extra p tags notify other users.
subjectThe PR title.
tOptional labels; may repeat.
cThe commit at the tip of the PR branch.
cloneAt least one git clone URL where the commit can be downloaded.
branch-nameOptional recommended branch name.
eOptional. Marks the PR as a revision of an existing root patch, which should then be closed.
merge-baseOptional. The most recent common ancestor with the target branch.

Pull request updates

A kind 1619 PR update moves the tip of an existing pull request. It references the PR with the uppercase NIP-22 tags: E holds the PR event ID and P holds the PR author.

Wire format - kind 1619 PR update
{
  "kind": 1619,
  "content": "",
  "tags": [
    ["a", "30617:<base-repo-owner-pubkey>:<base-repo-id>"],
    ["r", <earliest-unique-commit-id-of-repo>],
    ["p", <repository-owner>],
    ["p", <other-user>],              // optional

    ["E", <pull-request-event-id>],   // NIP-22 root reference
    ["P", <pull-request-author>],

    ["c", <current-commit-id>],       // updated tip of the PR
    ["clone", <clone-url>, ...],
    ["merge-base", <commit-id>]      // optional
  ]
}

6 Issues and Replies

A kind 1621 issue is a Markdown conversational thread about the repository: a bug report, a feature request, a question, or a comment of any kind. Like patches, send issues to the relays in the announcement's relays tag.

Wire format - kind 1621 issue
{
  "kind": 1621,
  "content": <markdown text>,
  "tags": [
    ["a", "30617:<base-repo-owner-pubkey>:<base-repo-id>"],
    ["p", <repository-owner>],
    ["subject", <issue-subject>],   // optional header
    ["t", <issue-label>],           // optional, repeatable
    ["t", <another-issue-label>]
  ]
}

Clients may display the subject tag as a header. One or more t tags label the issue.

Replies. Replies to an issue (kind:1621), a patch (kind:1617), or a pull request (kind:1618) follow the NIP-22 comment format.

7 Status Events

Every root patch, pull request, and issue has a status. The status defaults to Open. Status events change it. Four kinds exist:

Status kinds
KindMeaning
1630Open.
1631Applied or Merged for patches and PRs; Resolved for issues.
1632Closed.
1633Draft.
Wire format - kinds 1630–1633 status
{
  "kind": <1630|1631|1632|1633>,
  "content": <markdown text>,
  "tags": [
    ["e", <issue-or-PR-or-original-root-patch-id-hex>, "", "root"],
    ["e", <accepted-revision-root-id-hex>, "", "reply"], // when a revision applied
    ["p", <repository-owner>],
    ["p", <root-event-author>],
    ["p", <revision-author>],

    // optional, for better subscription filters
    ["a", "30617:<base-repo-owner-pubkey>:<base-repo-id>", <relay-url>],
    ["r", <earliest-unique-commit-id-of-repo>],

    // optional, kind 1631 only
    ["q", <applied-or-merged-patch-event-id>, <relay-url>, <pubkey>], // for each
    ["merge-commit", <merge-commit-id>],          // when merged
    ["r", <merge-commit-id>],
    ["applied-as-commits", <commit-id-in-master-branch>, ...], // when applied
    ["r", <applied-commit-id>]                     // for each
  ]
}

Two rules resolve conflicts:

  • The valid status is the most recent status event by created_at, from either the author of the issue or patch, or from a maintainer.
  • A patch revision inherits the status of its root patch. Exception: when the root patch is 1631 (Applied/Merged) and the revision is not tagged in that 1631 event, the revision counts as 1632 (Closed).
Tech Note

The source phrases the revision rule as "The Status of a patch-revision is to either that of the root-patch" — the sentence is garbled. The restatement above follows the evident intent. The spec also leaves inline file comments undefined; it lists them under "possible things to be added later" and expects separate kinds for patches and merged files.

Contributor Maintainer 1617 patch ["t","root"] → repo relays NIP-22 comment: "please rebase" 1617 patch ["t","root-revision"], e-reply → root 1631 Applied - merge-commit + q tags The maintainer applies the revision. The root patch reads Applied. Untagged revisions of that root now count as Closed.
Fig. 34-2 — Patch lifecycle. Patch, review comment, revision, then a status event closes the loop.

8 User Grasp List

A grasp server is a combined relay and Git server for NIP-34 activity. A kind 10317 replaceable event lists the grasp servers a user prefers, in order. It works like the NIP-65 relay list and the NIP-B7 blossom server list.

Wire format - kind 10317 user grasp list
{
  "kind": 10317,
  "content": "",
  "tags": [
    ["g", <grasp-service-websocket-url>]  // zero or more, in order of preference
  ]
}

The event should include one g tag per grasp service WebSocket URL, ordered by preference.

9 Worked Example

Carol maintains the project orange-peeler. Dave sends a one-commit patch. Carol applies it and publishes the status. Orange lines go to the relay. Blue lines come from the relay.

Session transcript - wss://relay.ngit.example
-- Carol announces the repository --
→ ["EVENT", {"kind":30617,"pubkey":"c4a01…","content":"","tags":[
     ["d","orange-peeler"],["name","Orange Peeler"],
     ["description","peels oranges over websockets"],
     ["clone","https://git.example.com/carol/orange-peeler.git"],
     ["relays","wss://relay.ngit.example"],
     ["r","94ba1e2…","euc"],["t","cli"]], …}]
← ["OK", "77ff01…", true, ""]

-- Dave sends a root patch to the repo's relay --
→ ["EVENT", {"kind":1617,"pubkey":"da4e9…",
     "content":"From 8f0c11… Mon Sep 17 00:00:00 2001\nFrom: Dave…\nSubject: [PATCH] fix peel angle\n…",
     "tags":[["a","30617:c4a01…:orange-peeler"],["r","94ba1e2…"],
     ["p","c4a01…"],["t","root"],
     ["commit","8f0c11…"],["parent-commit","94ba1e2…"],
     ["commit-pgp-sig",""],
     ["committer","Dave","dave@example.com","1750000000","-120"]], …}]
← ["OK", "e3d2ab…", true, ""]

-- Carol applies it and publishes the status --
→ ["EVENT", {"kind":1631,"pubkey":"c4a01…","content":"Applied, thanks!",
     "tags":[["e","e3d2ab…","","root"],["p","c4a01…"],["p","da4e9…"],
     ["a","30617:c4a01…:orange-peeler","wss://relay.ngit.example"],
     ["r","94ba1e2…"],["q","e3d2ab…","wss://relay.ngit.example","da4e9…"],
     ["applied-as-commits","8f0c11…"],["r","8f0c11…"]], …}]
← ["OK", "b9a844…", true, ""]

-- anyone can now clone via the announcement --
git clone nostr://npub1carol…/relay.ngit.example/orange-peeler

10 Version History

NIP-34 started as fiatjaf's sketch of "git stuff" and grew through heavy iteration by DanConwayDev, the author of the ngit tooling. Notable changes, newest first:

  • 20262026-06Replaced the old personal-fork marker with the u tag for subordinate forks; aligned with GRASP servers. Grasp hosting instructions later left the PR description spec.
  • 20262026-04Added the nostr:// clone URL format. Removed an unused refs tag extension and an incorrect claim about NIP-09 and state reset.
  • 20252025-10Added pull request (kind 1618) and PR update (kind 1619) events.
  • 20252025-03Deprecated the old git reply format in favor of NIP-22 comments.
  • 20242024-09Issues gained subject and t label tags; repository announcements gained optional t hashtags (2025-02).
  • 20242024-05Added repository state refs; simplified the r earliest-unique-commit tag (2024-04).
  • 20242024-03One week after origin: status events, cover letter rules, the repo-id standard, extra maintainers, and discoverability tags.
  • Origin2024-03-05fiatjaf published "NIP-34: git stuff" as PR #997.

Original Proposal - Credits

Authorfiatjaf
First commit2024-03-05 — “NIP-34: git stuff” (PR #997)
Statusdraft - optional — implemented by ngit, gitworkshop.dev, and grasp servers
Source34.md on GitHub - full change history

11 See Also

  • NIP-01 — the base protocol: events, addressable kinds, and the a/e/p/r/t tag machinery used here.
  • NIP-10 — the e reply marker that chains patches in a series.
  • NIP-22 — the comment format for replies to patches, PRs, and issues, and the E/P tags in PR updates.
  • NIP-19 — the naddr and npub encodings inside nostr:// clone URLs.
  • NIP-05 — DNS-based addresses, also usable in nostr:// clone URLs.
  • NIP-65 — the relay list that the user grasp list imitates.
  • NIP-B7 — the blossom server list, the other model for kind 10317.