ngit-grasp

ngit-grasp
by DanConwayDev _@danconwaydev.com

A self-hostable Nostr relay and Git server for decentralized repositories.

ngit-grasp is a production-ready Rust implementation of GRASP (Git Relays Authorized via Signed-Nostr Proofs). It combines a Nostr relay with Git Smart HTTP, validates repository writes against signed Nostr state, and synchronizes repositories across decentralized Git hosting providers.

First release: Aug 20, 2026, 12 total releases.

Most recent release: Nov 4, 2026.

Website

Appears in 0 app stacks.

0 sats / 0 zaps received in the past year.

Sats Received

Underlying data available via MCP: app_zaps, app_releases.

Zap Count

Underlying data available via MCP: app_zaps, app_releases.

Releases

  • Nov 4, 2026 3.1.1
    ### Security - Validate repository identifiers before they are used in filesystem paths in the deletion, recovery, and `/prs/` endpoints. Crafted identifiers in pull requests could previously traverse outside the intended repository directory. - Gate git object identifiers to their expected hex format and separate them from options on `git fetch` and related commands. Maliciously formed identifiers could previously be interpreted as command options. - Prevent git child processes from inheriting the relay's sensitive environment variables, including the operator's signing key.
    More…
    - Cap git push request bodies at a configurable size (default 512 MiB) and reject oversized pushes before they are buffered into memory. Compressed bodies are measured after decompression to stop amplification attacks. - Escape repository names and other user-controlled text on the landing and error pages, so a repository named after HTML or JavaScript cannot inject code into relay pages. - Move relay-owner key material generated at startup into files readable only by the service account, and continue refusing to load a key from an insecurely-permissioned file.
  • Nov 3, 2026 3.1.0
    ### Added: Blossom syncing, retention and serving ngit-grasp can now sync, retain and serve blobs referenced by repository events via a loosely coupled Blossom service. These include release assets, CI logs and artifacts, and images and videos attached to issues and pull requests. This is an opt-in feature, disabled by default. Run a compatible Blossom service alongside ngit-grasp at the relay endpoint, using built-in request forwarding, or connect an existing external Blossom service. Select individual repositories, whitelist repository owners (automatically including
    More…
    newly hosted repositories), or explicitly enable syncing for all hosted repositories. Retention follows the repository lifecycle. Storage budgets limit overall usage, blob categories and per-repository CI shares. Repositories can borrow spare capacity; when space is needed, older CI blobs from repositories above their shares are evicted first. Historical scanning respects original expiry dates, expands verified CI artifact manifests, and gives new references priority over backfill. The feature includes persistent ownership tracking, restart recovery, reconciliation, metrics and refusal diagnostics. Supporting memory-pressure improvements avoid mistaking inactive file cache for pressure and measure memory stalls before slowing background work. See [Sync, retain and serve repository blobs with Blossom](docs/how-to/retain-repository-blobs.md) for setup, signing keys, backend retention requirements, budgets and rollback. Blossom support is still maturing: start with a limited scope and monitor storage.
  • Oct 28, 2026 3.0.10
    ### Security - Update `rustls-webpki` to 0.103.16 and `webpki-roots` to 1.0.11 to reject distrusted emSign certificate paths (RUSTSEC-2026-0382 and RUSTSEC-2026-0381) in outbound Nostr TLS connections. - Update OpenSSL to 3.5.9 LTS in Nix builds and Linux release archives, including the September 2026 security fixes. **NixOS operators: update OpenSSL as well.** Update your deployment's nixpkgs pin to obtain patched OpenSSL, then rebuild and activate your NixOS configuration.
    More…
    The ngit-grasp NixOS module uses OpenSSL from your deployment's nixpkgs, so bumping only ngit-grasp does not update it. ### Fixed - Restrict announcement-expiry deletion to repositories with no admitted announcement. Preserve admitted maintainer views, coordinate cleanup with uploads and promotion, and recheck replacements and extended deadlines. Retry database or deletion failures; apply the same admission check to older checkpoints and both expiry phases. Already missing views are not restored. ### Documentation - Require local and remote `stable` updates in the stable-release checklist, alongside `master` and the version tag, with post-push verification.
  • Oct 27, 2026 3.0.9
    ### Security - Independently verify synced event IDs and signatures after read-only rejection checks and before admission side effects. Rejected events retained for dependency recovery still verify before caching. Verify transient fetch results before deduplication. Forged copies cannot poison recovery for the genuine event with the same claimed ID. - Reject non-global IPv4 destinations embedded in well-known NAT64 and 6to4 addresses. Reject the deployment-specific local-use NAT64 prefix entirely. - Constrain both NIP-11 probe paths to authorized, pinned destination addresses;
    More…
    disable redirects and environment proxies; cap response bodies at 256 KiB. Thanks to Cameron / SovTech for reporting these issues and describing fixes. **Optional stored-event check:** these issues were found by code review; no exploitation has been reported to us. Upgrading enables the admission checks without requiring a historical scan. Operators who want to check previously retained data can stop the upgraded relay, back up its data, and run `ngit-grasp event-integrity-check --relay-data-path <relay-dir> --git-data-path <git-dir>`. Inspect the report before deciding whether to rerun with `--execute` to purge invalid payloads. Keep the relay stopped throughout inspection and repair; this is not a live background scan. After any repair, run the report-only command again before restarting. See [stored-event verification and repair](docs/how-to/verify-stored-events.md) for complete commands and recovery limits. This does not undo historical Git or deletion side effects automatically. ### Fixed - Make staging test commits deterministic across clock boundaries so owed-history recovery checks do not fail when fixture creation crosses a second. - Keep test assertion output with Cargo diagnostics in CI log tails while preserving the test command's failure status. - Journal pending event saves before Git admission side effects, and retry transient database failures with capped exponential backoff for up to 30 minutes. Revalidate policy on retry, recover pending work after restart, and expire unsaved PR refs and their event-specific family retention through lifecycle cleanup. Direct submissions acknowledge completed writes or explicit duplicates; purgatory promotions no longer mistake a backend rejection for a saved event. ### Added - Add `event-integrity-check` to report and optionally purge invalid stored event payloads across the relay's databases and pending-event checkpoints. Include deletion-lifecycle cleanup and reset unverifiable negative caches so genuine events can be recovered after repair.
  • Oct 18, 2026 3.0.8
    Harden NIP-77 reconciliation, escape reflected 404 paths, tighten tag push authorization, and restore staged-history compatibility with older Git versions. ### Changed - Refresh workspace dependencies, including base64 0.23, bitcoin_hashes 1.2, and tokio-tungstenite 0.30. ### Security
    More…
    - Update negentropy to 0.5.2 to reject malformed NIP-77 ID counts before allocating memory, preventing peer-triggered memory exhaustion. ### Fixed - Fix staged history promotion and PR event acceptance on Git versions older than 2.42 by passing retained-history exclusions as command-line arguments instead of revision input. Thanks to Holly for the fix. - Escape the requested path displayed on generic 404 pages so HTML-like input is rendered as text rather than markup. Thanks to mptf for the fix. - Reject tag creation and updates absent from the signed repository state in the per-ref push validator. This adds defense in depth to the existing whole-state authorization check and preserves deletion of undeclared tags. Thanks to mptf for the fix.
  • Oct 2, 2026 3.0.7
    Reduce unnecessary sync traffic, reserve Git capacity for advertised commits, and stop subscription discovery promptly during shutdown. ### Fixed - Stop the self-subscriber when its sync-manager consumer exits or shutdown arrives, including while a startup batch is blocked. Avoid repeated channel-closed errors for every remaining relay. - Count individual outbound Git commands against a shared domain budget and
    More…
    reserve capacity for advertised commits during cold archive sync. Bound speculative OID fetches and report explicit Git rate-limit rejections. - Skip paused peers before scheduling purgatory dependency fetches, avoiding redundant retries and warnings while preserving retry eligibility on recovery. - Retire old relay subscriptions before reconnecting so SDK replay cannot accumulate untracked subscriptions or exhaust peers' retained-filter limits. - Remove rolled-back live subscriptions from the SDK registry so authentication cannot replay groups whose local permits have already been released.
  • Sep 30, 2026 3.0.6
    Keep unsigned Git uploads temporary, reclaim abandoned PR data, and preserve pending uploads correctly across crashes. ### Fixed - Keep Git uploads that no signed event names out of permanent storage. A push to `refs/nostr/<event-id>` whose PR event is not yet known is staged in the repository that received it, promoted into shared storage when the event is accepted, and reclaimed after its pending ref expires. Pushes backed by a signed State or PR event are stored as before. Empty staged `/prs/`
    More…
    repositories are removed after their staging metadata is reclaimed. - Accept a GRASP-06 `/prs/` PR event whose pushed ref survived a crash that lost its purgatory placeholder, matching it by the event's service-local clone URL and exact `refs/nostr/<event-id>` ref. - Remove abandoned normal-endpoint PR refs when their pending event expires, retaining exact repository scopes across restarts and retrying failed cleanup.
  • Sep 25, 2026 3.0.5
    Improve live sync startup, bound retries against unhealthy relays, fix Git push races and stalled uploads, and refresh compatible dependencies. ### Fixed - Let relays establish live sync while other relays reconcile history, by moving slow reconciliation and paced historic requests out of the shared sync actor into bounded background workers. Space connection starts at least 250 ms apart while retaining the eight-worker connection cap. - Apply independent exponential cooldowns to repeated rate-limit refusals and
    More…
    incomplete sync requests, from 65 seconds up to one hour. Queued requests respect active policy, rate-limit, and request-failure pauses; reconnects also enforce transport backoff. A successful connection alone does not reset recovery backoff. Preserve working live subscriptions and pending live repairs while history waits without occupying global worker capacity. - Back off failed participant mailbox fetches and NIP-65 identity queries independently per source, from five minutes up to one hour. Apply their deadlines to query admission and discovery-only reconnects, preserving pauses across reconnects and inventory growth, including newly discovered authors. Only a successful query of the corresponding type resets its failure delay; independent repository and live-sync work remains eligible. - Pause deterministic invalid or oversized filters instead of retrying unchanged requests. Regroup only explicit filter-count refusals, and ignore those for already-retired subscriptions so late responses cannot incorrectly pause replacement coverage for 24 hours. - Reconcile NIP-77 against the actual local event inventory on the intended relay, avoiding downloads of already-stored history. Keep reconciliation read-only and incoming payloads subject to the normal write policy. - Reuse installed core live filters during historic retries, preventing duplicate subscriptions from exhausting relay budgets. - Retire partially admitted or incomplete historic batches on admission failure, timeout, or closure before EOSE. Release their subscriptions, bound SDK retention with an auto-close deadline, and leave coverage unconfirmed for recovery instead of retaining stalled work or accepting partial history. Account for locally stored events and delivered events retained for dependency recovery before declaring history incomplete, avoiding repeated payload fetches and false remote-history failures. Preserve dependency IDs and relay hints for later maintainer changes without fetching Git data before state authorization. Bound recovery per event ID so progress on new events cannot renew older failures' retry budgets, rotate pending IDs fairly, and include bounded ID samples in background recovery logs. - Pace descendant fallback cycles with a one-minute refresh delay, retaining overlap and immediate baselines for changed frontiers. Report missing semantic fallback metadata as scheduled recovery rather than a subscription-creation error, and move routine batch-completion bookkeeping to debug logs. - Make concurrent thin-repository creation idempotent for the same identifier family and object format, preserving the winning view's refs and rejecting conflicting existing paths. - Accept Git pushes whose targets were already applied by background state promotion after ref advertisement, while retaining authorization for changed targets and Git's protection against concurrent ref updates. Validate PR refs independently so mixed pushes do not require state authorization for unchanged branches. Verify already-completed deletions without writing refs, including when mixed with new updates. Preserve per-ref outcomes for ordinary pushes and hold verified refs locked across atomic pushes. Keep rejected refs from being immediately overwritten by post-push state promotion. - Drain Git output while uploading push data, preventing pipe deadlocks when receive-pack produces progress or diagnostics before consuming the full pack. Require complete Git status reports and a successful subprocess exit before completing merged push responses, avoiding false success after interruption. - Reject PR and PR-update events missing commit metadata as terminal invalid events, avoiding repeated recovery attempts for immutable malformed data. - Omit raw rejected relay payloads from SDK diagnostics while retaining URLs and rejection reasons. Include repository, process exit status, and stream outcome in Git fetch failure diagnostics, and log client-cancelled upload-pack streams at debug level. ### Changed - Refresh compatible Cargo dependencies, including hyper-util 0.1.21 and transitive library and build-tool updates. Keep the Nostr SDK/database and WebSocket API versions unchanged.
  • Sep 21, 2026 3.0.4
    ### Fixed - Fix queued sync requests bypassing relay rate-limit backoff and continuing to send requests during the cooldown. Rate-limit signals now take effect even when sync event processing is blocked, and repeated signals no longer flood warning logs. - Preserve repository membership while a pending announcement is saved, so concurrent reconciliation cannot restore obsolete Git state. - Retain live roots received while their accepted repository announcement is waiting for batch processing, so their discovery work is not lost.
    More…
    ### Dependencies - Update compatible Cargo dependencies, including `nostr-sdk` 0.45.4. The embedded relay now closes affected live subscriptions when their event buffer overflows, allowing clients to detect gaps and resubscribe for stored events. It also checks the connection limit before the WebSocket handshake. - Update `negentropy` to 0.5.1, reducing allocations during reconciliation.
  • Sep 18, 2026 3.0.3
    This patch improves repository-state correctness, startup recovery, and relay responsiveness, and makes audit probes more accurate. It also includes released Nostr SDK fixes and more deterministic test fixtures. ### Fixed - Scope read-only audit state selection to the selected repository's confirmed maintainers, excluding unrelated repositories with the same identifier. Audit probes now also report unexpected branches and tags, including refs left behind when the winning state is empty.
    More…
    - Select one complete repository state in the read-only audit probe, using the lower event ID for same-second ties. Older states no longer contribute removed branches or tags to the expected Git refs. - Honor lower-ID same-second replacements in the public owner-repository sync helper while preserving replay suppression. - Restore relay-owner profiles and relay lists using the lower event ID when fetched versions share a timestamp. - Honor same-second lower-ID replacements when de-listing served repositories, capturing superseded history, and choosing rollback or post-deletion state. History capture time no longer overrides the original events' ID tie-break. - Apply the lower-event-ID tie-break to same-second purgatory replacements. Git push authorization now selects the preferred matching state regardless of arrival order, and a winning announcement that removes this service evicts its purgatory entry and associated repository data. - Preserve all persisted root events when rebuilding sync state at startup. Purgatory cleanup can no longer remove a partially reconstructed repository and leave its older threads untracked until another restart. - Upgrade the Nostr SDK and database dependencies to released 0.45 patch versions. NIP-77 database scans now run off async workers, avoiding runtime stalls that can delay unrelated relay connections. Reconciliation excludes expired events, and NIP-77 continuation messages no longer consume the query-start allowance. - Pick up upstream relay validation fixes that reject reposted protected events and verify the declared proof-of-work target as well as the event hash. - Require EOSE before accepting background discovery history, instead of treating a disconnected or timed-out partial response as a completed fetch. - Preserve background discovery relay backoff across idle-connection cleanup so unavailable mailbox and profile sources do not restart their retry history. - Render metrics on a blocking worker and serialize scrapes so repository counting does not occupy async workers serving relay and Git requests. - Keep periodic recovery checkpoints off async workers, release snapshot locks before disk I/O, and finish active checkpoints before the shutdown snapshot. - Avoid delayed-acknowledgement stalls in small WebSocket and Git responses by enabling `TCP_NODELAY` on accepted connections.
  • Sep 11, 2026 3.0.2
    This release contains no production runtime changes. It improves release and deployment plumbing, source-owned documentation exports, canonical project references, and build-time test portability. ### Added - Export a deterministic, side-effect-free command and configuration schema for pinned downstream documentation generation. ### Changed
    More…
    - Publish tagged static archives as signed NIP-82 releases, using the `main` channel for stable versions and a channel derived from the prerelease label for release candidates. - Publish release container images as Nostr kind-30624 repositories backed by redundant Blossom storage. Stable images receive version and `latest` tags; prereleases receive version and channel tags. Maintainers can backfill an exact prior-release tag through CI without moving a stable channel backwards. - Point GRASP implementation references and maintainer guidance at their canonical Nostr Git sources. ### Fixed - Make shallow-migration and bare-repository test fixtures deterministic across Git default-branch configurations, while keeping routine `git init` output out of test logs.
  • Aug 20, 2026 3.0.0
    ### Summary ngit-grasp 3.0.0 adds self-hosted private repositories through GRASP-08, with the protocol designed so hosting providers can offer managed private services later. Proactive Sync Plus (GRASP-03) enables maintainer-based moderation and simpler, more performant Nostr Git clients. This major release also includes significant security and performance fixes;
    More…
    an immediate upgrade is recommended. Its Git storage migration makes the v2-to-v3 upgrade one-way without restoring a pre-upgrade snapshot. Plan for downtime while the first v3 startup migrates Git data. The largest GRASP service tested so far, `relay.ngit.dev`, took approximately 51 minutes. ### Breaking changes - Version 3 performs an automatic, one-way migration of `NGIT_GIT_DATA_PATH` from complete per-owner and `/prs/` repositories to thin views backed by shared identifier families. Version 2 does not coordinate reads and writes through this layout and must not be run against migrated Git data. Rollback requires restoring the Git and relay-data snapshot taken before the v3 launch, not only downgrading the binary. - The v3 storage migration finishes before the HTTP server starts listening, so operators must plan for relay downtime during the upgrade. The largest GRASP service migrated so far spent 51 minutes migrating 2,294 repository views into 2,014 identifier families, retired 2,291 backups, and retained three for automatic integrity repair. - Maintainer authorization now uses reciprocal membership. A pubkey listed by a confirmed maintainer is only invited and cannot publish authoritative repository state until its own kind-30617 announcement for the same identifier lists back an existing confirmed maintainer. At release time, this changes the effective authorization state of zero live repositories; it is breaking for future invitations because they now require an explicit acceptance step. Repository announcements now support the NIP-34 `M` (lead maintainer), `m` (co-maintainer), and `o` (moderator) role tags. Active `M` and `m` entries define maintainers; `o` does not grant repository-state authority. The deprecated `maintainers` tag is used as a fallback only when none of those role tags are present. - Added `trusted_proxy_cidrs` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. - Added `base_path` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. - Added `startup_integrity_identifiers` to the public `Config` struct. Rust consumers that construct `Config` with a struct literal must provide it. ### Security - Reject unsuitable peer-advertised NIP-65 relay URLs before they can occupy mailbox selection slots, overlays, probe queues, or metrics: URLs failing the static outbound-target checks, `.onion` hosts (temporary mailbox policy), URLs matching nostr-watch's 52-word generated-path rule, 64-hex path segments, hostname-repeating path segments, and URLs over 2,048 bytes. These are single-URL rules; relay lists are never rejected wholesale, and legitimate path-based relays (`/inbox`, `/outbox`, language and versioned paths, semantic queries) remain accepted. - Canonicalize accepted NIP-65 relay URLs and bound every per-author selection to at most four relays per purpose, taken in published tag order after deduplication so trailing-root-slash and default-port variants of one relay consume a single slot. Live inbox coverage and owner/maintainer repository inboxes select `read`/unmarked relays, identity refresh selects `write`/unmarked relays, and participant history prefers `write`/unmarked relays with read-only relays filling remaining slots. The bound is a hard internal constant, so one hostile or machine-generated kind `10002` list cannot inflate mailbox inventory. Canonical keys now match the connection map, fixing trailing-root-slash mailbox relays that previously could never start their history probe. - Bound every sync-actor notification channel and require `auth-required` retry IDs to own a current subscription-ledger permit. Repeated terminals, forged subscription IDs, and delayed worker results can no longer create unbounded retained memory. - Added opt-in trusted-proxy CIDRs so WebSocket connection policy, per-IP metrics, abuse indicators, and logs can use the real client address without trusting spoofable forwarding headers from arbitrary direct peers. - Reject non-canonical and path-traversing repository coordinates after URL decoding across repository landing pages, Git Smart HTTP, and GRASP-06. All tagged releases through v2.1.2 could resolve crafted requests outside the requested repository namespace, allowing unauthenticated reads of Git repositories accessible to the service. With GRASP-06 enabled, crafted PR submissions could also write Git objects and PR refs into another hosted repository without its maintainer authorization. Any deployment that enabled GRASP-06 on a tagged build through v2.1.2 should treat its hosted Git repositories as potentially containing unauthorized objects or refs until it completes the v3 integrity checks. Conforming Nostr Git clients that resolve refs from valid signed State, PR, and PR Update events are not expected to accept this unauthorized data; direct Git consumers do not have that protection. Operators must upgrade to v3.0.0. By default, every v3 startup runs a non-blocking authorization-integrity pass that compares every served branch, tag, `HEAD`, and `refs/nostr/*` ref with the accepted State, PR, and PR Update events (including precisely scoped in-flight events). Owner-view PR refs require either confirmed-maintainer overlap with the target or an exact standard clone URL naming that owner and identifier on the service; similar, foreign, and `/prs/` URLs do not count. It repairs unambiguous differences and emits `manual_inspection=true` errors without deleting unexplained PR refs that may be evidence. GRASP-06 operators must check those logs and the terminal `Git authorization-integrity startup pass completed` summary; any non-zero `manual_inspection` or `failed` count means the named repository still requires review. `NGIT_STARTUP_INTEGRITY_IDENTIFIERS` can temporarily scope both startup integrity passes for release-candidate validation, but scoped logs establish integrity only for the named repositories. Operators must remove the scope and complete the default all-family sweep before treating the v3 upgrade as complete. The live `integrity-check --identifier` command now compares storage and event authorization together; it is check-only by default and applies safe fixes with `--repair`. ### Added - Build a reproducible, statically linked x86_64 Linux archive and checksum from every version tag. - Add `NGIT_BASE_PATH` / `--base-path` (NixOS `basePath`) so one GRASP server can be mounted below a shared domain path. WebSocket, Git Smart HTTP, GRASP-06, NIP-11, metrics, icons, landing pages, service matching, generated NIP-65 identity, and self-sync all use the configured prefix. Requests outside it are rejected. Path-mounted servers neither serve nor advertise the root-domain NIP-05 identity, and their generated kind-0 profile omits the `nip05` field. - Publish the relay-owner identity on startup as a minimal kind-0 profile with the scheme-less public URL as `name`, `bot: true`, and `_@domain` NIP-05 for domain-root deployments, plus a kind-10002 list naming this relay as its sole read/write relay. An operator-customized profile survives restarts, and no identity event — stored or generated — is published before the local database and at least one user-index relay have been checked for that kind. Every send is preceded by a per-relay re-check: an identity found on a user-index relay (for example after a local database wipe) is adopted locally and never overwritten, so the relay only fills identity gaps on the indexes, retrying transient failures with a capped backoff. In private mode the identity is seeded locally but never published, so a private relay's existence is not advertised. The relay also trusts events signed by its own key so that key can operate an `ngit-ci` coordinator, but only for kinds without a dedicated admission policy: owner-signed NIP-34 repository events pass the normal announcement, state, and PR policies, and NIP-09/NIP-62 tombstones still prevent replaying retracted owner events. - Serve the relay owner's public key as the NIP-05 `_@domain` identity from `/.well-known/nostr.json`. NIP-11 advertises NIP-05 only when requested at the domain root (`/`); relays mounted below a path do not claim support. - Add fixed-cardinality aggregate metrics for important long-lived sync state and document the producer, cleanup owner, bound, and terminal behavior of every peer-influenced transient subsystem. - Add bounded, operator-configurable inbox fallback coverage when a successful Sync+ user-index query finds no accepted NIP-65 relay list for a root author. - Add a default-on `NGIT_SYNC_PLUS_ENABLED` opt-out and advertise GRASP-03 in NIP-11 only when the Sync+ overlay is effective. - Recover repository-event descendants which reference a direct thread member but omit the repository and root-event tags. When the per-connection ledger can retain complete descendant coverage while preserving control and historic capacity, those filters stay live; otherwise bounded EOSE-closing history provides eventual coverage. Direct replaceable and addressable members contribute both their event ID and NIP-01 coordinate, covering descendants which use `a`, `A`, or coordinate-valued `q` tags. The frontier is deliberately non-recursive and connection sharding remains unnecessary. ### Changed - Retire identifier-family migration backups family by family. Once every view of a family is installed, each backup is verified (family contains every Git-readable backup object, thin views match their journal snapshot, family packs are indexed and valid), and the ordinary family integrity report must be healthy before deletion. Unhealthy families retain their backups for automatic repair. Healthy backups are deleted before the next family migrates, bounding peak migration disk overhead to the family in flight. Unindexed legacy packs are quarantined by content under `.grasp/migration/unindexed-packs/`, unverifiable backups are retained with a warning, backup-directory removals are fsynced before their journals are removed, and completed installations whose backups were already removed manually start unchanged. - Detect and reconcile incomplete object graphs inherited from legacy storage. The v3 migration preserves the legacy repository and its backup; the online integrity worker then requests missing objects from every accepted clone source and logs any family that remains incomplete without making it less available. A server-side `shallow` marker specifically identifies repositories created by the depth-one fallback in untagged development builds between 2026-01-05 (`623cae5`) and 2026-01-12 (`f25eea8`); no tagged v1 or v2 release shipped that behavior. Missing ancestors without a `shallow` marker are not attributed to that bug and can require a complete maintainer clone or bundle when every announced server inherited the same incomplete history. - Scope bare log levels to ngit-grasp while keeping dependencies at warnings; explicit tracing filter expressions remain unchanged. - Keep per-event discovery and validation details at debug, retain aggregate sync outcomes at info, and report unsupported NIP-77 once per connection instead of warning once per filter. - Treat routine client disconnects, incomplete HTTP/WebSocket sessions, and Git missing-object probes as debug diagnostics while preserving internal service and database failures as errors. - Metrics compatibility: removed the `ngit_sync_naughty_relay_info{relay,category,reason}` metric because its relay and raw reason labels were peer-controlled and unbounded. Use the unchanged `ngit_sync_naughty_relays_total{category}` metric for fixed-cardinality aggregate quarantine counts. - Minimise live-subscription churn as repository coverage grows: stable full core groups and descendant groups remain open. The mutable core tail is repacked in full when doing so releases slots; otherwise only the smallest useful subset is replaced to absorb new filters. This accounts for both filter-count and serialized-byte limits. Repository filter chunks are deterministic, and failed tail replacement restores the previous tail. ### Fixed - Restore cold rejected-maintainer recovery after policy admission pre-saves the newly authorized announcement for membership reconciliation. An exact duplicate from that intentional pre-save now still broadcasts the announcement and expands its state/Git dependencies, while a genuinely superseded replaceable event remains classified as a duplicate without downstream fan-out. - Discover historical repository roots and descendants from the NIP-65 inboxes of accepted repository owners and maintainers. Public Sync+ instances probe exact repository coordinates and every known root on those authors' bounded, sanitized read/unmarked inboxes through paced, byte-bounded history workers. This coverage is deliberately historical-only: owning or maintaining a repository never adds an author's inbox relays to ordinary persistent live repository targets, while accepted root authors retain the pre-existing live inbox coverage. Private instances continue to withhold repository coordinates, and the ordinary write policy and persistent deletion tombstones remain authoritative. - Retire peer-closed outbound subscriptions from rust-nostr's desired registry without interrupting NIP-42: the first `auth-required` response keeps the same subscription and ledger slot for one authenticated retry, while a repeat refusal becomes a 24-hour policy pause. This prevents authentication replay and live repair from multiplying rejected IDs without bound. Policy detection precedes descendant fallback, while an unclassified live closure waits for the ordinary cooldown before one repair attempt. - Bound duplicate repository Git acquisition to one primary and one delayed distinct-domain hedge, and recover from silent transports without imposing a total fetch deadline. Continuously active large transfers remain unbounded; a process group silent for five minutes is terminated gracefully then forcibly, after which remaining sources are tried sequentially. Concurrent object acquisition does not write `FETCH_HEAD`, run automatic maintenance, or infer source speed from a shared object database.