cop is one command for the clipboard, wherever you happen to be. It picks the
right backend for the machine you're on (pbcopy, wl-copy, xclip, or OSC 52
over SSH) so you don't have to remember which one lives where.
echo "hello" | cop # copy stdin
cop file.txt # copy a file's contents
cop -p # paste to stdout
cop -p out.txt # paste into a filePipe something in to copy it, run cop -p to get it back. Same two commands on
macOS, on Linux under Wayland or X11, and inside an SSH session. When you want
more, it can also sync between machines through a small cloud relay (encrypted by
default), append to the clipboard, copy a whole directory at once, and drop
starter files like .gitignore into a project.
homeshick clone gh:goude/cop
homeshick link copThat symlinks ~/.homesick/repos/cop/home/bin/cop to ~/bin/cop. There is
nothing else to configure. pas is a symlink to cop for pasting, so pas and
cop -p do the same thing.
Optional fish completions:
cop --completions fish > ~/.config/fish/completions/cop.fishecho "hello" | cop # copy text
cop file.txt # copy file contents
cop < file.txt # same, via redirect
cop dir/ # copy every file in dir/, each labelled by path
cop -p # paste to stdout
cop -p out.txt # paste into a file
pas # paste (pas is a symlink to cop)
echo "more" | cop -a # append to what's already on the clipboard-t (--tee) passes input straight through to stdout while it copies, so you
can watch a long command run and still keep its output:
make 2>&1 | cop -t # the build scrolls past live; the full log lands on the clipboardcop --notes opens (or creates) a NOTES.md in the current directory with your
$EDITOR.
cop -n moves text between machines through a small Cloudflare Worker. Two
things to know before you use it:
It encrypts by default. You set COP_SECRET, and the text is encrypted
(AES-256-CBC with an HMAC for integrity) before it leaves your machine. A fetch
with cop -pn detects the format and decrypts it for you. If COP_SECRET isn't
set, cop -n refuses rather than send anything in the clear.
It always asks first. Every upload shows a short preview and waits for y/N.
Nothing is sent silently.
export COP_SECRET=your-passphrase
echo "hi" | cop -n # asks, then encrypts and uploads
cop -pn # fetches and decrypts to stdout
cop -pnc # also copies the result locallyThe read endpoint is public, so if you genuinely want to send plaintext you have to say so:
echo "public note" | cop -n --plain # warns, asks, then uploads as-is
cop -pn --plain # fetch the raw stored valueReading never needs a secret. Pushing needs COP_WRITE_SECRET (see below).
A note on the crypto: the MAC is checked before anything is decrypted, so a tampered or wrong-key value fails instead of returning garbage. The check uses a plain string compare rather than a constant-time one, which is fine for a personal clipboard with a high-entropy secret over HTTPS but worth knowing.
Files cop can copy into the current directory:
| Template | What it is |
|---|---|
.gitignore |
A broad gitignore for most projects |
.editorconfig |
EditorConfig with common defaults |
NOTES.md |
A NOTES.md stub |
cop --templates # list them
cop --template .gitignore # copy one into ./Names prefix-match, so cop --template git resolves to .gitignore.
The default endpoint is the author's personal worker, so for real use you'll
want your own. Point cop at it with environment variables:
export COP_SERVICE_URL=https://your-worker.workers.dev/cop
export COP_WRITE_SECRET=... # the push token; reads don't need itThe worker is a single file, cloudflare/worker.js. GET is public, POST requires
a Bearer token. To deploy it:
- Create a Worker (Cloudflare dashboard, Workers & Pages, Create) and paste in
cloudflare/worker.js. - Create a KV namespace and bind it to the worker under the name
COP_STORE. - Add a secret named
COP_WRITE_SECRET, for exampleopenssl rand -hex 32. - Deploy, and note the
*.workers.devURL.
Keep COP_WRITE_SECRET out of any repo. It belongs in Cloudflare and in the
shell environment of the machines that push. Put both variables in your shell
profile to keep them across sessions. Since encryption is on by default, what
sits in the KV store is ciphertext; only --plain sends would be readable by
anyone who knows the URL.
Layout:
home/bin/cop bootstrap: resolves its real path and sources the modules
lib/cop/
ui.sh colours, help text, content_stats, completions
clipboard.sh backend detection, OSC 52
crypto.sh base64 and authenticated encryption (cop_encrypt/cop_decrypt)
network.sh worker calls and the upload confirmation
templates.sh template copy and list
tests.sh self-test suite
core.sh do_copy, do_paste, main
templates/ the template files themselves
The bootstrap sources modules in dependency order (ui, clipboard, crypto,
network, templates, tests, core) and then calls main.
Checks before committing:
cop --test # self-tests; network round-trips run only if COP_WRITE_SECRET is set
shfmt -i 2 -d home/bin/cop lib/cop/*.sh # formatting (shfmt is the formatter)
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck \
-x -P SCRIPTDIR home/bin/cop lib/cop/*.sh # lintTo add a template, drop a file into lib/cop/templates/. It shows up in
cop --templates immediately.
To add a module, create lib/cop/<name>.sh, add a source line for it in
home/bin/cop before anything that depends on it, and run cop --test.