Skip to article content All articles

Building a GitHub Backed Pastebin CLI in Go

A local-first CLI design for fast terminal notes, crash-safe autosave, explicit Git synchronization, and conflict-safe recovery.

Abhishek Choudhury

Abhishek Choudhury

April 20, 2026 · Updated July 29, 2026 · 7 min read

Hands feeding colorful code cards through a compact versioning machine
On this page7 min left

At a glance

  1. Local-first means the editable source of truth remains useful without a network; remote storage adds synchronization and durability.

  2. Autosave and version history solve different problems: recovery snapshots protect drafts, while explicit commits create meaningful durable versions.

  3. Safe synchronization must expect divergence and preserve both versions instead of silently choosing a winner.

01The product contract

A sharp tool for one interrupted workflow

There is a particular kind of friction that appears when you are deep in a terminal and need to keep a JSON payload, command sequence, or debugging note. Opening a browser or heavyweight notes application breaks the flow. Sending proprietary text to an arbitrary paste service creates a different problem: you have lost control of where the data lives.

The useful constraints are more important than the implementation language: the tool must open instantly, work offline, recover unsaved edits, keep human-readable files, synchronize across machines, and avoid owning a new server.

Choosing the storage boundaryEach option can store text. The useful distinction is who owns offline behavior, conflict semantics, authentication, and operations.
DecisionLocal-firstLocal files + GitRemote-firstGitHub Gist or APIProduct platformCustom sync service
Best forPersonal snippets that must remain editable offline and portable.Small remote snippets when offline editing is not the primary requirement.Teams, sharing policies, search, collaboration, or non-Git clients.
You ownFile layout, autosave, Git workflow, and conflict experience.Local cache, API retries, revision mapping, and rate-limit behavior.API, database, identity, migrations, uptime, backups, and sync protocol.
Trade-offExcellent transparency; synchronization semantics must be designed carefully.Less Git plumbing, but the network service becomes part of every important path.Maximum control with the largest operational surface.
ExamplesPrivate repository, normal commits, local recovery directory.Gist API, provider-hosted revision history.Dedicated service with records, accounts, and device synchronization.

Local-first

Local files + Git

Best for
Personal snippets that must remain editable offline and portable.
You own
File layout, autosave, Git workflow, and conflict experience.
Trade-off
Excellent transparency; synchronization semantics must be designed carefully.
Examples
Private repository, normal commits, local recovery directory.

Remote-first

GitHub Gist or API

Best for
Small remote snippets when offline editing is not the primary requirement.
You own
Local cache, API retries, revision mapping, and rate-limit behavior.
Trade-off
Less Git plumbing, but the network service becomes part of every important path.
Examples
Gist API, provider-hosted revision history.

Product platform

Custom sync service

Best for
Teams, sharing policies, search, collaboration, or non-Git clients.
You own
API, database, identity, migrations, uptime, backups, and sync protocol.
Trade-off
Maximum control with the largest operational surface.
Examples
Dedicated service with records, accounts, and device synchronization.

02Ownership

Local-first is a behavior, not a cache

A cache assumes the authoritative copy lives elsewhere. A local-first tool treats the local file as immediately useful and editable, then synchronizes when a remote is available. The local-first software paper describes broader ideals around offline work, ownership, longevity, and collaboration. A personal CLI needs only a focused subset, but the distinction still matters.

  • Offline is normal. Creating, opening, editing, listing, and searching local snippets should not require GitHub.
  • Files stay legible. A user can inspect, copy, back up, or version the data without the application.
  • Sync is explicit. The tool clearly reports whether work is local-only, synchronized, diverged, or conflicted.
  • Remote failure is contained. Authentication or network problems do not destroy the current draft.

Loading diagram…

  1. Open or create a snippet in the terminal editor.
  2. Debounced edits produce an atomic local recovery snapshot.
  3. The journal records dirty state and the recovery generation.
  4. An explicit save updates the working file and creates a meaningful Git commit when content changed.
  5. An explicit sync fetches and compares local and remote history.
  6. Unambiguous histories fast-forward or push.
  7. Diverged histories preserve a conflict copy before reconciliation.
  8. A resolved history is pushed to the private GitHub repository.
The local-first snippet lifecycle. Editing and recovery remain local. Meaningful versions and remote synchronization happen only at explicit boundaries.

03Durability

Use different layers for different promises

One file cannot simultaneously represent the editor buffer, crash recovery, a user-approved version, and a synchronized remote state. Giving those promises separate layers makes failure behavior much easier to explain.

Loading diagram…

  1. The editor buffer contains the current in-memory text.
  2. A local recovery snapshot protects recent edits from a process crash.
  3. The working file contains content the user explicitly accepted.
  4. A Git commit creates a named durable version and history.
  5. The private remote repository provides an authenticated off-device copy.
  6. The journal records how these layers relate and which work remains pending.
Five layers of durability. Recovery, accepted content, version history, and off-device synchronization are separate promises and should use separate artifacts.
  • Working file: the user-visible content that opens immediately and can remain local.
  • Recovery snapshot: a short-lived, crash-safe copy written frequently while editing.
  • Journal: compact metadata describing dirty files, recovery generations, last known remote revision, and pending operations.
  • Git commit: an intentional durable version with a stable identity and history.
  • Remote repository: an authenticated synchronization target and off-device copy.

Use the operating system’s application-data or configuration directory rather than inventing a path under the current working directory. Go exposes this boundary through os.UserConfigDir, with platform-specific behavior.

04Crash safety

Autosave without manufacturing history

Autosave should protect the draft without creating a remote commit for every keystroke. A practical loop debounces edits, writes a temporary file in the same filesystem, flushes it when the platform supports that guarantee, and atomically renames it over the recovery snapshot.

func SaveRecovery(path string, body []byte) error {
    tmp := path + ".tmp"

    file, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
    if err != nil {
        return err
    }

    if _, err = file.Write(body); err != nil {
        file.Close()
        return err
    }
    if err = file.Sync(); err != nil {
        file.Close()
        return err
    }
    if err = file.Close(); err != nil {
        return err
    }

    return os.Rename(tmp, path)
}

05Remote durability

Make save and sync separate decisions

A local save confirms the contents the user wants to keep. A sync reconciles that version with the remote repository. Keeping those commands separate avoids coupling everyday editing to connectivity and makes it possible to describe failures precisely.

  1. Create a stable local version. Write the working file, clear its accepted recovery snapshot, and create a commit only when content changed.
  2. Fetch before integrating. Learn whether the remote advanced since the last synchronized revision.
  3. Fast-forward when possible. If only one side changed, apply the unambiguous update.
  4. Merge only from a clean baseline. Do not start reconciliation with unrelated uncommitted changes.
  5. Push only a resolved history. Never hide a conflict behind a successful-looking sync message.

Git’s merge documentation explains the states created by a non-trivial merge. A CLI can wrap those states, but should not pretend they do not exist.

06Divergence

Preserve both versions before asking who wins

Two machines can edit the same snippet before either synchronizes. Automatic line merging is useful when changes do not overlap, but a personal text tool should prefer recoverability over cleverness when intent is ambiguous.

Loading diagram…

  1. Require a clean managed worktree before synchronization.
  2. Fetch remote history without modifying the working file.
  3. If neither side changed, report synchronized.
  4. If only local changed, push the local commit.
  5. If only remote changed, fast-forward the local branch.
  6. If both changed, create a conflict copy before attempting reconciliation.
  7. If the merge is clean, commit and push the resolved history.
  8. If intent remains ambiguous, stop and report the conflict paths and next action.
A synchronization decision tree. The sync command never silently selects a winner. Divergence becomes a visible, recoverable product state.
Suggested conflict-copy identity

Use a stable path derived from the snippet name plus device and timestamp, for example notes/today.conflict-laptop-20260729T103000Z.txt. Record the relationship in the journal so listing and cleanup commands can explain why the file exists.

07Trust boundary

Reuse authentication, but define the privacy limit

Delegating authentication to GitHub CLI avoids inventing token storage. The GitHub CLI authentication flow normally stores credentials in the system credential store and documents its fallback behavior. The Pastebin CLI should call gh auth status and fail with a specific setup instruction rather than copying tokens into its own configuration.

  • Use restrictive local permissions. Create data, journal, and recovery files for the current user only.
  • Avoid secrets in process arguments. Arguments can appear in shell history or process listings.
  • Redact diagnostic output. Logs should identify paths and operations without printing snippet contents or credentials.
  • Make repository visibility explicit. Verify the created remote is private and stop if its visibility changes unexpectedly.

08Distribution

Install without taking over the machine

A single Go binary is a strong fit for a personal CLI, but distribution still needs integrity, ownership, and rollback. Install into a user-owned binary directory, verify release checksums, and keep upgrades explicit or policy-driven.

set -euo pipefail

install_dir="${XDG_BIN_HOME:-$HOME/.local/bin}"
mkdir -p "$install_dir"

# Download the release and its published checksum to a temporary directory.
# Verify before replacing the currently installed binary.
sha256sum --check pb_SHA256SUMS
install -m 0755 pb "$install_dir/pb"
  • Do not require administrator access. The tool owns only its user-level binary and application directories.
  • Do not replace a working binary before verification. Stage and verify the new release, then rename it into place.
  • Rate-limit update checks. Cache the last check and let users choose automatic, prompted, or manual upgrades.

09Implementation sketch

A sync contract that refuses data loss

The core sync function should return structured states rather than only printing Git output. That keeps the command interface honest about whether it synchronized, stayed local, or produced a conflict.

func Sync(ctx context.Context, entry JournalEntry) (SyncResult, error) {
    if err := repo.RequireCleanWorktree(ctx); err != nil {
        return SyncResult{}, err
    }

    remote, err := repo.Fetch(ctx)
    if err != nil {
        return SyncResult{State: LocalOnly}, err
    }

    relation := repo.Compare(entry.LastSyncedCommit, remote.Head)
    switch relation {
    case Unchanged:
        return SyncResult{State: Synchronized}, nil
    case LocalAhead:
        return repo.Push(ctx)
    case RemoteAhead:
        return repo.FastForward(ctx, remote.Head)
    case Diverged:
        conflictPath, err := repo.PreserveConflictCopy(entry.Path)
        if err != nil {
            return SyncResult{}, err
        }
        return repo.MergeWithConflictPath(ctx, remote.Head, conflictPath)
    default:
        return SyncResult{}, ErrUnknownHistory
    }
}

10Before release

A local-first CLI checklist

  1. Test offline first. Every local command works when DNS, GitHub, or authentication is unavailable.
  2. Interrupt every write. Kill the editor and process during snapshot, journal, commit, fetch, merge, and upgrade operations.
  3. Simulate divergence. Edit the same file on two machines and verify that neither version disappears.
  4. Protect path boundaries. Reject traversal, symlink escape, reserved names, and writes outside the managed root.
  5. Verify permissions and redaction. Inspect created files, subprocess arguments, and diagnostic logs.
  6. Make every state visible. Users can distinguish dirty, recovered, local-only, synchronized, and conflicted snippets.

11Working vocabulary

Glossary

Local-first
Software whose primary working copy remains useful and editable on the local device without depending on a network request.
Recovery snapshot
A short-lived crash-safe copy used to restore edits that were not yet accepted as a durable version.
Journal
Structured local metadata recording dirty state, recovery generations, pending work, and synchronization identity.
Atomic rename
Replacing a file through a filesystem rename so readers observe either the previous complete version or the new complete version.
Fast-forward
Advancing a Git branch to a descendant commit without creating a merge commit.
Divergence
A history state where local and remote sides both contain commits not present on the other side.
Conflict copy
A preserved alternate version created before reconciliation so no ambiguous edit is silently discarded.

12Sources and further reading

References

  1. Building a GitHub Backed Pastebin CLI in Go

    Abhishek Choudhury

    Original project article, published April 2026.
  2. Local-First Software: You Own Your Data, in Spite of the Cloud

    Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan

    The principles and trade-offs behind local-first software.
  3. Package os: UserConfigDir

    The Go Authors

    Platform-aware application configuration directories.
  4. git-merge documentation

    The Git Project

    Merge states, conflicts, abort behavior, and safe preconditions.
  5. GitHub CLI: gh auth login

    GitHub

    Authentication modes and credential-storage behavior.

From the archive

Keep reading

View all articles