Skip to content
/ pbj Public

the universal clipboard you always wanted. One command. All your devices. Encrypted. Distributed. Zero bullshit.

Notifications You must be signed in to change notification settings

ahoward/pbj

Repository files navigation

🥜🥪 pbj 🍇🥪

the universal paste buffer

peanut butter, and fucking jelly.


TL;DR 🚀

pbj is the universal clipboard you always wanted. One command. All your devices. Encrypted. Distributed. Zero bullshit.

# copy on laptop
cat secret.txt | pbj

# paste on phone, desktop, server, wherever
pbj > secret.txt

Similar to pbcopy, xclip, wl-copy — but universal. All your machines sync automatically. Mobile, desktop, servers, toasters. If it has git and gh, it syncs.


How It Works 🔮

The Secret Sauce:

  1. 🔐 AES-256-GCM encryption (because privacy)
  2. 🧩 10MB chunks (generous free tier abuse)
  3. 🔑 GitHub Secrets for key backup (auto-synced on every copy!)
  4. 📦 Git repo as sync backend (public or private - data is encrypted!)
  5. Background push & key backup (async, non-blocking)

Data Flow:

stdin → encrypt → chunk → git commit → background push + key backup
                                              ↓
                                         (GitHub)
                                              ↓
git pull ← decrypt ← reassemble ← read chunks → stdout

Storage Layout:

~/.pbj/                    # your forked repo (code + data together!)
├── bin/
│   └── pbj               # the script you run
├── data/                 # encrypted clipboard data (COMMITTED TO GIT!)
│   ├── clip.0000.enc     # encrypted chunk 0
│   ├── clip.0001.enc     # encrypted chunk 1
│   ├── clip.0002.enc     # ...
│   └── manifest.json     # chunk metadata
├── .pbj-key              # encryption key cache (gitignored)
├── prune.log             # background prune log (gitignored)
├── README.md             # this file
└── .gitignore            # only ignores .pbj-key and prune.log

The repo contains both code AND your encrypted data!

  • Code lives in bin/ (version controlled)
  • Data lives in data/ (version controlled, encrypted, synced!)
  • Data is COMMITTED to your fork (it's encrypted, so safe)
  • Git sync = clipboard sync across devices
  • You run from your own fork

Installation 🛠️

Step 1: Fork on GitHub

  1. Go to https://github.com/ahoward/pbj
  2. Click "Fork" (top right)
  3. Optional: Make it private if you want (recommended but not required)

Fork cleanup: When you fork, you'll get ahoward's encrypted clipboard data:

  • You can't decrypt it (you don't have his key)
  • On first run, pbj detects the key mismatch and auto-clears it
  • Then you start fresh with your own encrypted data

Step 2: Clone Anywhere (Recommended: ~/.pbj)

# Recommended location: ~/.pbj
git clone git@github.com:YOUR_USERNAME/pbj.git ~/.pbj

# But you can clone ANYWHERE:
git clone git@github.com:YOUR_USERNAME/pbj.git ~/my-pbj
git clone git@github.com:YOUR_USERNAME/pbj.git /opt/pbj

# Or use HTTPS
git clone https://github.com/YOUR_USERNAME/pbj.git ~/.pbj

Note: The script automatically detects where it's installed. Data is stored relative to the clone location in data/.

Step 3: Add to PATH

# For ~/.pbj (recommended):
echo 'export PATH="$HOME/.pbj/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# For custom location:
echo 'export PATH="/your/custom/path/pbj/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Or for zsh
echo 'export PATH="$HOME/.pbj/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Step 4: Verify

which pbj
# Should output: /home/you/.pbj/bin/pbj (or your custom location)

pbj help
# Should show usage

Alternative: Symlink

# Clone anywhere, symlink to /usr/local/bin
git clone git@github.com:YOUR_USERNAME/pbj.git ~/pbj
sudo ln -s ~/pbj/bin/pbj /usr/local/bin/pbj

Requirements:

  • Ruby (stdlib only, no gems)
  • gh CLI (authenticated: gh auth login)
  • git

Usage 💻

Copy (Write Mode)

# pipe anything
echo "hack the planet" | pbj
cat image.png | pbj
curl https://example.com | pbj

# or redirect
pbj < document.pdf

Paste (Read Mode)

# stdout
pbj

# to file
pbj > output.txt

# pipe it
pbj | grep secret | awk '{print $2}'

Cross-Device Sync

# on your laptop
echo "meeting notes" | pbj

# on your phone (termux)
pbj  # gets the same notes

# on your server
pbj | mail -s "notes" you@example.com

Clipboard History

# view history (last 20 entries by default)
pbj history

# view more
pbj history 50

# output:
# Clipboard History:
# ================================================================================
# [0] 2025-10-25 14:32:01 (a3f2c91)
#     hack the planet
#
# [1] 2025-10-25 14:30:15 (b4e1a82)
#     meeting notes for tomorrow...
#
# [2] 2025-10-25 12:15:33 (c7d9f31)
#     <binary data>

# paste from history by index
pbj 0  # most recent
pbj 1  # second most recent
pbj 5  # sixth entry

# examples
pbj 2 > old-clipboard.txt
pbj 0 | grep "important"

Key Recovery (PIN-Protected)

# Recover encryption key on a new device
pbj recover

# This will:
# 1. Back up existing key to .pbj-key.bak (if overwriting)
# 2. Trigger the key-recovery workflow
# 3. Poll until workflow completes (30-60 seconds)
# 4. Prompt for your 4-digit PIN
# 5. Decrypt and save the key automatically
# 6. Remind you to delete the workflow run

# You'll be prompted:
# Enter your PIN: ****
# ✓ Key decrypted successfully

# Alternative: Manual key copy (fastest!)
# Device 1: cat ~/.pbj/.pbj-key
# Device 2: echo "PASTE_KEY_HERE" > ~/.pbj/.pbj-key

# Set or update your PIN
pbj set-pin

Multi-device setup:

  1. Fork ahoward/pbj once on GitHub
  2. Clone to ~/.pbj on Device 1
  3. Add ~/.pbj/bin to PATH
  4. First pbj copy prompts for 4-digit PIN
  5. Key + PIN stored in GitHub Secrets (encrypted backup)
  6. Clone same fork to Device 2
  7. Copy key manually (fastest!) OR use pbj recover (requires PIN)

Key Storage (PIN-Protected):

  • Primary: .pbj-key file in repo (gitignored, fast)
  • PIN: 4-digit PIN for key recovery (prompted on first copy)
  • Backup: GitHub Secrets store both PBJ_KEY and PBJ_PIN (write-only)
  • Auto-backup: Key + PIN synced to GitHub Secrets in background
  • Recovery: Workflow encrypts key with PIN before logging (split-key security)
  • Multi-device: Manual copy (recommended) or pbj recover (requires PIN)

Method 1: Manual copy (RECOMMENDED - fast and simple)

# Device 1:
cat ~/.pbj/.pbj-key

# Device 2:
echo "PASTE_KEY_HERE" > ~/.pbj/.pbj-key
chmod 600 ~/.pbj/.pbj-key

Method 2: Automatic recovery with pbj recover (PIN-protected)

# On Device 2 (automatic - triggers workflow, polls, decrypts key):
pbj recover

# The command will:
# 1. Back up existing key to .pbj-key.bak (if overwriting)
# 2. Trigger key-recovery workflow
# 3. Wait for workflow to complete (30-60 seconds)
# 4. Prompt for your 4-digit PIN
# 5. Decrypt key and save to ~/.pbj/.pbj-key
# 6. Remind you to delete the workflow run

# You'll be prompted:
# Enter your PIN: ****
# ✓ Key decrypted successfully

# Then delete the workflow run:
gh run delete <run-id> -R username/pbj

# If something went wrong, your old key is at:
# ~/.pbj/.pbj-key.bak

Method 3: Manual workflow (if pbj recover fails)

# On Device 2:
cd ~/.pbj
gh workflow run key-recovery

# Wait 30 seconds, then view logs:
gh run list --workflow=key-recovery
gh run view <run-id> --log

# Copy key from logs to ~/.pbj/.pbj-key
# IMPORTANT: Delete the workflow run after (exposes key in logs!)

Features 🎯

End-to-end encrypted (AES-256-GCM) ✅ Automatic chunking for large files ✅ GitHub Secrets key management ✅ Async background sync (non-blocking) ✅ Works with binary data (images, PDFs, whatever) ✅ Git-based history (time-travel your clipboard) ✅ History browsing (pbj history + pbj N) ✅ Cross-platform (Linux, macOS, BSD, wherever Ruby runs) ✅ Zero external dependencies (stdlib only) ✅ Robust error handling (clear error messages)


Architecture 🏗️

Why this approach wins:

Problem Solution
Distribution Fork repo → every user has their own
Auth gh CLI (already configured)
Sync Git push/pull (built-in conflict resolution)
Privacy AES-256-GCM encryption (never trust the cloud)
Size limits 10MB chunks (abuse free tier forever)
Key management GitHub Secrets + auto-retrieval
History Git commits (time-travel your clipboard)
Binary data Base64? Nah. Raw encrypted bytes.
Code updates git pull upstream (from ahoward/pbj)

Security Model:

  • Encryption key stored in GitHub Secrets (write-only, cannot be read via API)
  • Local key cache in .pbj-key (gitignored, fast access)
  • Key fingerprint (SHA256 hash) stored in manifest for mismatch detection
  • Each chunk encrypted independently with AES-256-GCM
  • Authenticated encryption (GCM) prevents tampering
  • Encrypted data IS committed to your fork (safe because encrypted)
  • Even if someone accesses your repo, they can't decrypt without the key

Why GitHub Secrets (not Variables)?

  • Secrets: Write-only, can't read via API → secure even on public repos ✅
  • Variables: Readable by anyone on public repos → dangerous ❌
  • Trade-off: Manual key copy required, but security is worth it

Key Recovery:

  • Auto-backup: Key synced to GitHub Secrets automatically on every copy (background, non-blocking)
  • Primary: Copy .pbj-key file manually between devices (fast, simple)
  • Backup: Use key-recovery workflow to retrieve from Secrets (slow but works)
  • Workflow exposes key in logs temporarily (delete run after use)
  • Key always up-to-date in Secrets (even if regenerated)

Key Fingerprinting:

  • Each manifest contains SHA256 hash of encryption key (first 64 bits)
  • On paste, fingerprint is checked against current key
  • Mismatch = auto-cleanup (prevents using wrong key)
  • Prevents corruption from forked repos with someone else's data

Fork-based Distribution:

  • Everyone forks ahoward/pbj to their own repo
  • Code in bin/ is version controlled
  • Data in data/ is version controlled (encrypted!)
  • Each fork contains that user's encrypted clipboard
  • Updates: git remote add upstream https://github.com/ahoward/pbj.git
  • Pull updates: git pull upstream main (merges code, preserves your data)

Hacking 🔧

Chunk size:

CHUNK_SIZE = 10 * 1024 * 1024  # 10MB

Repo location:

REPO_DIR = File.join(ENV['HOME'], '.pbj')

Encryption:

  • Algorithm: AES-256-GCM
  • IV: 12 bytes (random per encryption)
  • Auth tag: 16 bytes
  • Key: 32 bytes (stored in GitHub Secrets)

History Management:

MAX_HISTORY_COMMITS = 108       # Keep last 108 entries
REPO_SIZE_WARNING_MB = 420      # Warn at 420MB
AUTO_PRUNE_THRESHOLD_MB = 4242  # Auto-prune at 4.2GB

Why these limits?

  • Git repos store full history (every clipboard entry ever)
  • Large files accumulate quickly (even with chunking)
  • GitHub free tier: repos should stay under 1GB (recommended)
  • 108 entries with auto-prune at 4.2GB gives you headroom
  • Auto-pruning runs in background after sync (non-blocking)
  • pbj info shows current size and warnings
  • pbj prune manually cleans up when needed
  • Background prune activity logged to ~/.pbj/prune.log

Troubleshooting 🐛

"⚠ KEY MISMATCH DETECTED"

This is normal when you first fork the repo! It means:

  • You forked ahoward/pbj (or someone else's fork)
  • The data was encrypted with their key
  • You have a different key

What happens automatically:

⚠ KEY MISMATCH DETECTED
============================================================
The clipboard data was encrypted with a different key.

This usually happens when you:
  1. Forked someone else's repo (contains their data)
  2. Cloned on a new device (different key)
  3. Deleted your encryption key

Data fingerprint: a1b2c3d4e5f6g7h8
Your fingerprint:  z9y8x7w6v5u4t3s2

🗑️  Clearing incompatible data...
✓ Data cleared. You can now start using pbj with your key.

Solution: Just start using pbj normally:

echo "my first clipboard" | pbj
pbj  # Should output: my first clipboard

"✗ Not authenticated with gh"

gh auth login

"✗ Repository not found at ~/.pbj"

# You forgot to fork and clone!
# 1. Fork https://github.com/ahoward/pbj on GitHub
# 2. Clone to ~/.pbj:
git clone git@github.com:YOUR_USERNAME/pbj.git ~/.pbj

# 3. Add to PATH:
echo 'export PATH="$HOME/.pbj/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

"✗ Decryption failed (wrong key or corrupted data)"

This means your devices have different encryption keys. Fix:

Option 1: Copy key file between devices

# On Device A (has working key):
cat ~/.pbj/.pbj-key

# On Device B (needs key):
# Copy the output and paste:
echo "PASTE_KEY_HERE" > ~/.pbj/.pbj-key
chmod 600 ~/.pbj/.pbj-key

Option 2: Use GitHub Secrets (if you set it up)

# Manually retrieve from Secrets
gh secret list -R username/pbj
# (Note: gh can't read secret values directly - they're write-only)

Option 3: Start fresh (loses history)

rm ~/.pbj/.pbj-key
rm -rf ~/.pbj/data
echo "new start" | pbj

"✗ Missing chunks"

cd ~/.pbj && git pull    # sync latest changes
pbj                      # retry

"✗ Failed to sync to remote"

  • Non-fatal warning (still works locally)
  • Check network connection
  • Verify gh auth status

Git conflicts:

  • Automatic recovery (git reset --hard origin/main)
  • Last-write-wins model
  • Manual: cd ~/.pbj && git status

View all errors:

pbj 2>&1 | tee pbj-debug.log

Repository size issues:

pbj info                 # check current size
pbj prune                # keep last 108 entries
pbj prune 50             # keep last 50 entries

# Manual cleanup
cd ~/.pbj
git gc --aggressive --prune=now

Auto-pruning:

  • Automatically triggers at 4242MB
  • Runs in background after each copy (non-blocking)
  • Keeps last 108 clipboard entries
  • Warnings logged to ~/.pbj/prune.log
  • Check log: tail ~/.pbj/prune.log
  • Non-destructive (can disable by editing constants)

License 📜

Do whatever you want. It's peanut butter and jelly.


Made with 🥜 and 🍇 by hackers, for hackers.

About

the universal clipboard you always wanted. One command. All your devices. Encrypted. Distributed. Zero bullshit.

Resources

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •