Skip to article content Blogs

Building a GitHub Backed Pastebin CLI in Go

Why I stopped using bloated note apps and built a local-first, autosaving terminal tool for my daily snippets.

Hands feeding colorful code cards through a compact versioning machine

Summary

pb is a small CLI I built in Go to save terminal snippets as plain files and sync them through a private GitHub repository. It works offline, autosaves without filling the history with junk commits, and keeps both copies when two machines disagree.

Why I built it

There is a specific frustration that shows up when you are deep in the terminal and need somewhere to drop a snippet. My day at ValueLabs is distributed systems, tracing logs, and debugging backend logic, and when I am knee-deep in Python and Django configuration, switching context is the enemy.

Opening a browser to paste a JSON payload or a stack trace into some web pastebin breaks that flow. It is also a privacy risk for proprietary code. Note-taking apps have the same problems for this job: they make you leave the terminal, they eat memory, and they lock your text into their own format.

So I wrote down what I actually wanted. It had to work offline. It had to autosave, so closing the terminal by accident never costs a draft. It needed a history of changes. And it had to store everything on infrastructure I already trust.

Go for the binary, GitHub for the storage

My daily stack is Python and Next.js, but I wanted this to be fast and to ship as a single executable, and Go is the right tool for that.

Instead of spinning up a database to hold a few text files, I used something I already had: GitHub, a free, version-controlled backend. pb uses the official GitHub CLI for authentication, so I did not have to build token storage. The first time you initialise it, it creates a private repository called pastebin-cli-store on your account, and that repository is the backend for every snippet.

Loading diagram…

  1. Open or create a snippet in the terminal editor.
  2. Background autosave writes a local recovery snapshot.
  3. A journal tracks file metadata and pending operations.
  4. An explicit save creates a durable version.
  5. An explicit sync reconciles it with the private GitHub repository.
  6. If both sides changed, a conflict copy is kept.
The local-first snippet lifecycle. Editing and recovery stay on my machine. Versions and syncing only happen when I ask.

This is what people mean by local-first. The file on my machine is the real copy. GitHub is where it gets synced, not where it lives.

Autosave without junk history

Building a synced CLI is trickier than it looks, and autosave is where it starts. If every keystroke became a commit, the history would be useless. If nothing was saved until I asked, one closed terminal would lose the draft.

pb splits those two promises. While I type in the built-in editor, it writes recovery snapshots in the background, on my machine only. Its local data lives under the user configuration directory (Go’s os.UserConfigDir), where a journal tracks file metadata and pending operations. A durable version, the kind that reaches GitHub, is only created when I explicitly save or sync.

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. Each layer makes one promise and survives one kind of failure.

The safe way to write a snapshot like this is an old trick: write to a temporary file, flush it, then rename it over the previous one. A crash leaves either the old snapshot or the new one, never half of each. The code below is that pattern in outline, not pb’s exact source.

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)
}

Sync, and what happens when two machines disagree

I edit a snippet on my work laptop and open it at home. The sync command reconciles the two. Because saving and syncing are separate, editing never waits on the network, and a failed sync never costs me a saved draft.

Loading diagram…

  1. Editing makes a snippet an autosaved draft.
  2. Saving makes it a saved local version.
  3. Syncing sends it to GitHub.
  4. If another machine changed it too, a conflict copy is kept.
  5. Choosing a version returns it to synced.
The states a snippet moves through. Save and sync are separate steps, so a failed sync never costs a saved draft.

The hard case is when both machines changed the same snippet before either synced. pb does not blindly overwrite one with the other. It creates a conflict copy, so both versions survive and I choose. Losing a note silently is the one thing a notes tool must never do.

The store is a Git repository, so these are the cases Git’s merge documentation describes. A CLI can hide the commands, but it should not hide the outcome.

Installing it without sudo

The biggest hurdle with small internal tools is getting them onto a machine. I wanted pb to install without administrator rights.

There are shell installers for macOS and Linux and a PowerShell script for Windows. Each one pulls the compiled Go binary from GitHub releases and puts it in a bin directory the user owns.

pb also keeps itself current. It checks GitHub for a newer release about once a day, and the upgrade policy can be set to auto, prompt, or manual, so it never interrupts me mid-thought.

Small and sharp

Building pb was a good exercise in local-first design, and it fixed a very specific itch. I no longer keep terminal output in clunky web apps. I type pb new notes/today.txt, drop the code in, and save.

The best tools are not always big platforms. Sometimes they are the small, sharp utilities we build for ourselves.

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.