pull down to refresh

First public release — git-nostr-sign v0.2.1

If you live in both worlds (Git + Nostr), you have probably wished your commit signatures were just another signed event — same keys, same mental model, no GPG keyserver theatre. This tool is that experiment shipped.

What it does
— Each commit (and tag) gets a signature Git understands: OpenPGP-shaped armor around a JSON payload that is, under the hood, a Nostr event (kind 1639). git verify-commit and git log --show-signature work the way your muscle memory expects.
— Verification is nostr-tools + the commit bytes — no keyring sync, no platform TOS as your trust root for the crypto.

How it ships (why Nostr people should care)
— There is no npm registry artifact to trust blindly. Releases are announced as kind 1063 “package” events, signed by the publisher key you already follow or verify out-of-band. The tarball bytes live on Blossom (and mirrors); the event carries the sha256 you check before install.
— Bootstrap is intentionally boring: curl a small install.sh from HTTPS (same domain as NIP-05 when you set it up that way), then the script pulls the latest matching release from relays. Upgrades are git-nostr-update — same pipeline, no git pull of our repo required for end users.
— Publisher npub for this line of releases: @git-nostr-sign

How you sign
— Local nsec on the machine, or Amber over NIP-46 (including Nostr Connect QR in the setup wizard) so the signing key never leaves the phone if you prefer that threat model.

This is our first broad invitation to try it on real repos and CI boxes. Expect rough edges; issues and patches welcome wherever you track the source.

Install (one line):
curl -fsSL https://nostr-svrn.codeberg.page/install.sh | sh -s -- --npub git-nostr-sign@nostr-svrn.codeberg.page

🔗

$ docker run -it alpine /bin/ash -c "apk update && apk add curl nodejs npm && curl -fsSL https://nostr-svrn.codeberg.page/install.sh | sed 's/set -e/set -ex/' | sh"
v3.23.3-413-gfc223ba99f6 [https://dl-cdn.alpinelinux.org/alpine/v3.23/main]
v3.23.3-414-gbbf576395af [https://dl-cdn.alpinelinux.org/alpine/v3.23/community]
OK: 27583 distinct packages available
( 1/21) Installing brotli-libs (1.2.0-r0)
( 2/21) Installing c-ares (1.34.6-r0)
( 3/21) Installing libunistring (1.4.1-r0)
( 4/21) Installing libidn2 (2.3.8-r0)
( 5/21) Installing nghttp2-libs (1.68.0-r0)
( 6/21) Installing nghttp3 (1.13.1-r0)
( 7/21) Installing libpsl (0.21.5-r3)
( 8/21) Installing zstd-libs (1.5.7-r2)
( 9/21) Installing libcurl (8.17.0-r1)
(10/21) Installing curl (8.17.0-r1)
(11/21) Installing ca-certificates (20251003-r0)
(12/21) Installing libgcc (15.2.0-r2)
(13/21) Installing libstdc++ (15.2.0-r2)
(14/21) Installing ada-libs (3.3.0-r0)
(15/21) Installing icu-data-en (76.1-r1)
  Executing icu-data-en-76.1-r1.post-install
  * 
  * If you need ICU with non-English locales and legacy charset support, install
  * package icu-data-full.
  * 
(16/21) Installing icu-libs (76.1-r1)
(17/21) Installing simdjson (3.12.0-r0)
(18/21) Installing simdutf (7.5.0-r1)
(19/21) Installing sqlite-libs (3.51.2-r0)
(20/21) Installing nodejs (24.14.1-r0)
(21/21) Installing npm (11.11.0-r0)
Executing busybox-1.37.0-r30.trigger
Executing ca-certificates-20251003-r0.trigger
OK: 86.0 MiB in 37 packages
+ PUBLISHER_NPUB=git-nostr-sign@nostr-svrn.codeberg.page
+ RELAY=wss://relay.damus.io
+ BOLD='\033[1m'
+ GREEN='\033[32m'
+ YELLOW='\033[33m'
+ RED='\033[31m'
+ RESET='\033[0m'
+ '[' 0 -gt 0 ]
+ printf '\n\033[1m╔══════════════════════════════════════════╗\033[0m\n'

╔══════════════════════════════════════════╗
+ printf '\033[1m║      git-nostr-sign  ·  installer        ║\033[0m\n'
║      git-nostr-sign  ·  installer        ║
+ printf '\033[1m╚══════════════════════════════════════════╝\033[0m\n\n'
╚══════════════════════════════════════════╝

+ printf '  Publisher: %s\n' git-nostr-sign@nostr-svrn.codeberg.page
  Publisher: git-nostr-sign@nostr-svrn.codeberg.page
+ printf '  Relay:     %s\n' wss://relay.damus.io
  Relay:     wss://relay.damus.io
+ hdr 'Checking dependencies'
+ printf '\n\033[1m%s\033[0m\n' 'Checking dependencies'

Checking dependencies
+ command -v node
+ command -v pnpm
+ command -v npm
+ PKG_MGR=npm
+ node -e 'process.stdout.write(process.versions.node.split('"'"'.'"'"')[0])'
+ NODE_MAJOR=24
+ '[' 24 -lt 18 ]
+ node --version
+ ok 'Node.js v24.14.1'
+ printf '  \033[32m✓\033[0m  %s\n' 'Node.js v24.14.1'
  ✓  Node.js v24.14.1
+ ok 'Package manager: npm'
+ printf '  \033[32m✓\033[0m  %s\n' 'Package manager: npm'
  ✓  Package manager: npm
+ command -v sha256sum
+ SHA_CMD=sha256sum
+ ok 'SHA-256 available'
+ printf '  \033[32m✓\033[0m  %s\n' 'SHA-256 available'
  ✓  SHA-256 available
+ echo git-nostr-sign@nostr-svrn.codeberg.page
+ grep -q @
+ hdr 'Resolving NIP-05 address'
+ printf '\n\033[1m%s\033[0m\n' 'Resolving NIP-05 address'

Resolving NIP-05 address
+ echo git-nostr-sign@nostr-svrn.codeberg.page
+ cut -d@ -f1
+ NIP05_NAME=git-nostr-sign
+ + cut -d@ -f2
echo git-nostr-sign@nostr-svrn.codeberg.page
+ NIP05_DOMAIN=nostr-svrn.codeberg.page
+ NIP05_URL='https://nostr-svrn.codeberg.page/.well-known/nostr.json?name=git-nostr-sign'
+ printf '  Fetching %s\n' 'https://nostr-svrn.codeberg.page/.well-known/nostr.json?name=git-nostr-sign'
  Fetching https://nostr-svrn.codeberg.page/.well-known/nostr.json?name=git-nostr-sign
+ curl -fsSL 'https://nostr-svrn.codeberg.page/.well-known/nostr.json?name=git-nostr-sign'
+ NIP05_JSON='{
  "names": {
    "git-nostr-sign": "ea4e2c999d18c229c688e0cc31ae4c3d556f942537da0d8473d050097983d434"
  },
  "relays": {
    "ea4e2c999d18c229c688e0cc31ae4c3d556f942537da0d8473d050097983d434": [
      "wss://relay.damus.io",
      "wss://relay.nostr.band",
      "wss://nos.lol",
      "wss://relay.primal.net"
    ]
  }
}'
+ node -e '
    const j = JSON.parse(process.env.NIP05_JSON || '"'"'{}'"'"');
    const hex = j.names?.['"'"'git-nostr-sign'"'"'];
    if (!hex) { process.stderr.write('"'"'Name not found\n'"'"'); process.exit(1); }
    import('"'"'nostr-tools/nip19'"'"').then(m => process.stdout.write(m.npubEncode(hex)));
  ' 'NIP05_JSON={
  "names": {
    "git-nostr-sign": "ea4e2c999d18c229c688e0cc31ae4c3d556f942537da0d8473d050097983d434"
  },
  "relays": {
    "ea4e2c999d18c229c688e0cc31ae4c3d556f942537da0d8473d050097983d434": [
      "wss://relay.damus.io",
      "wss://relay.nostr.band",
      "wss://nos.lol",
      "wss://relay.primal.net"
    ]
  }
}'
Name not found
+ PUBLISHER_NPUB=
+ die 'Could not resolve NIP-05 name'
+ printf '  \033[31m✗\033[0m  %s\n' 'Could not resolve NIP-05 name'
  ✗  Could not resolve NIP-05 name
+ exit 1
reply

Looks like you're missing the --npub argument when piping to sh

reply
72 sats \ 6 replies \ @mf OP 2 Apr

Thank for the feedback. Patched 0.2.2 is available. Want to give another try?

reply

Like I indicated: missing nostr-tools/nip19

node:internal/modules/package_json_reader:301
  throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
        ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'nostr-tools' imported from /[eval]
    at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9)
    at packageResolve (node:internal/modules/esm/resolve:768:81)
    at moduleResolve (node:internal/modules/esm/resolve:859:18)
    at defaultResolve (node:internal/modules/esm/resolve:991:11)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:719:20)
    at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:736:38)
    at ModuleLoader.resolveSync (node:internal/modules/esm/loader:765:52)
    at #resolve (node:internal/modules/esm/loader:701:17)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:621:35)
    at onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:650:32) {
  code: 'ERR_MODULE_NOT_FOUND'
}

It be good if you test it yourself. Just use my pipe, it's easy:

docker run -it alpine /bin/ash -c "apk update && apk add curl nodejs npm && curl -fsSL https://nostr-svrn.codeberg.page/install.sh | sed 's/set -e/set -ex/' | sh"
reply
72 sats \ 3 replies \ @mf OP 2 Apr

I thought I did... 0.2.3 is now available.

reply

I get to the wizard now!

But when I should input it already quit the script.

Is there a way to just manually configure?

╔══════════════════════════════════════════════╗
║  git-nostr-sign  ·  setup wizard             ║
╚══════════════════════════════════════════════╝

  This wizard will:
    1. Set publisher identity (import nsec, Amber, or new keypair)
    2. Deploy a NIP-05 address to Codeberg Pages (free)
    3. Configure your git to sign commits with your Nostr key

  Press Ctrl+C at any time to cancel.


─── Phase 1 · Tool Identity ─────────────────────

Publisher identity (NIP-05 + release metadata)
  1.  Import existing nsec or hex     — reuse a key you already have
  2.  Amber (NIP-46)                  — private key stays on your phone
  3.  Generate new dedicated keypair  — fresh “project” publisher key

Enter number: / # 1
/bin/sh: 1: not found
reply
10 sats \ 1 reply \ @optimism 2 Apr

Tested through npm run setup, which doesn't terminate on the menu when ran like that.

Findings from setup process:

  1. QR cannot be read by Amber when using a standard macos terminal font
  2. Amber through bunker doesn't seem to work - the app never talks to Amber, according to Amber (others do) I tried with multiple relays
  3. When going the new keypair route (option 3), I get the npub echo'd to me, but then after skipping step 2 (because codeberg is still centralized, so let's not) I am asked to type the nsec. This is odd, because I was never given it. When I enter nothing, it terminates.

You really, really, really need more testing.

reply
72 sats \ 0 replies \ @mf OP 3 Apr

That's true. Appreciate the feedback.

0.3.0:

➤ git-nostr-setup -h

git-nostr-setup / npm run init -- …  —  interactive wizard (default)

Non-interactive (--non-interactive or -N):

  Identity (pick one):
    --generate-identity           New random keypair (commits + updater)
    --identity-nsec <nsec|hex>   Existing key (also: GNS_IDENTITY_NSEC)
    --bunker-url <bunker://…>   Amber NIP-46 (approve on phone; GNS_BUNKER_URL)
    --reuse-identity              Keep identity.json, refresh signing + git only

  Signing override (optional — at most one):
    --signing-nsec …             Different local key for commits
    --signing-bunker-url …       Different Amber bunker for commits

  Git (pick one):
    --git-global                 Global gpg.program + signing
    --git-local                  This repo only (must be inside a git work tree)
    --no-git-config              Only write config.json / identity.json

    --no-pre-push-hook           Skip core.hooksPath .githooks

  Examples:
    git-nostr-setup -N --generate-identity --git-global
    git-nostr-setup -N --identity-nsec "$GNS_IDENTITY_NSEC" --git-global
    git-nostr-setup -N --bunker-url 'bunker://…' --git-local

  NIP-46 still needs Amber approval the first time. QR: set GNS_QR_SMALL=1 for a smaller pattern.
  Secrets in argv hit shell history — prefer env vars.

Here's what the command does:

  1. docker run -it alpine /bin/ash -c ... Get clean alpine image, call its shell (interactively) to execute a script
  2. apk update && apk add curl nodejs npm ... install the prerequisites
  3. curl -fsSL https://nostr-svrn.codeberg.page/install.sh - your install script
  4. | sed 's/set -e/set -ex/' | sh - edit the set -e line to make it echo back so we can debug
reply

Same result with that. This command structure is taken from the site, not the post here

reply
73 sats \ 1 reply \ @unboiled 2 Apr

Right. Maybe you can set PUBLISHER_NPUB=... for the last command in-line.
It seems to look there.

reply

Yeah. I can edit the script. There's other problems too that I foresee in the immediate next step... But it doesn't even get there.

(It expects to have nostr-tools/nip19 but as you see in the log, that wasn't fetched)

reply