Pipe stdin into a running Emacs server. Two implementations, byte-identical CLI: a Rust binary and a hardened Bash 5.3+ script.
echo "hello, emacs" | epipe # async, fire-and-forget (default)
some-long-command | epipe --wait # block until Emacs returns
git diff | epipe --tee=diff # also print to terminal via bat
cat err.log | epipe -m eval -b "*errors*" --debugSame tool, two engines. Pick whichever fits the box you're on.
target/release/epipe (Rust) |
bash/epipe.bash |
|
|---|---|---|
| Runtime | Single static binary, ~400 KB | Bash 5.3+ + coreutils |
| Startup | ~1 ms | ~5 ms |
| Deploy | cargo install / scp |
cp to ~/scripts/, no compile step |
| Dependencies | md5 crate, libc |
md5sum/md5, mktemp, od, date |
CLI surface is locked: every flag, every default, every elisp form is identical. The same test suite runs against both binaries and asserts they emit equivalent commands to emacsclient.
cargo build --release
install -m 755 target/release/epipe ~/.local/bin/epipeinstall -m 755 bash/epipe.bash ~/.local/bin/epipeOr use both side-by-side as epipe and epipe.bash.
USAGE:
epipe [OPTIONS] [-- ELISP]
EMACS MODE:
-m, --mode MODE file | eval | inline (default: file)
file: emacsclient <tmpfile> (visits the file)
eval: --eval (insert-file-contents into *pipe*)
inline: --eval (insert "data") -- no temp file
-b, --buffer NAME Buffer name for eval/inline modes (default: *pipe*)
WAIT:
-w, --wait, --sync Block until Emacs returns
-n, --no-wait, --async Fire-and-forget (default)
TEMPFILE:
-d, --tmpdir DIR Temp directory (default: $TMPDIR or /tmp)
-p, --pattern PAT Filename pattern (default: epipe-{ts}-{rand})
Templates: {hash} {pid} {ts} {date} {rand} {user}
-k, --keep Keep tempfile after sending to Emacs
SIDE OUTPUTS:
-t, --tee[=LANG] Echo input to stdout (with bat -l LANG if installed)
-c, --cb, --clipboard Also copy input to system clipboard
VERBOSITY:
-s, --silent Errors only
-v, --verbose More output (-vv adds the elisp form)
--debug Logfmt records to stderr
-V, --version Print version and exit
-h, --help Show this help and exit
| Mode | What Emacs ends up doing | When to use |
|---|---|---|
file (default) |
emacsclient <tempfile> -- Emacs visits the file, the buffer is named after it, you can C-x C-s it back |
Pager-style: long output you want to navigate, search, save |
eval |
emacsclient --eval '(progn (switch-to-buffer (generate-new-buffer-name "*pipe*")) (insert-file-contents "..."))' |
Detached buffer with a name you control; original tempfile auto-deleted unless --keep |
inline |
emacsclient --eval '(progn (switch-to-buffer ...) (insert "..."))' -- payload is escaped into the elisp string itself |
Tiny snippets where you don't want a temp file at all |
Default is async: epipe returns immediately, Emacs handles the buffer in the background. Good for ad-hoc piping from shells, prompts, scripts.
--wait / --sync blocks until Emacs returns. Use this when:
- An LLM agent (Claude Code, forge, etc.) needs to wait for a human to act in Emacs before proceeding.
- You want the exit code from
emacsclientto propagate back. - You want the tempfile cleaned up immediately after Emacs reads it (file mode +
--wait+ no--keep).
--tee(or-t): also write stdin toepipe's stdout. With--tee=LANGandbatinstalled and stdout is a tty, syntax-highlight asLANG. Otherwise plain.--clipboard(or-c/--cb): also copy stdin to the system clipboard. Uses the first available ofpbcopy,wl-copy,xclip,xsel.
Combine freely:
kubectl get pods -o yaml | epipe --tee=yaml --clipboard --mode=eval -b "*pods*"| Flag | What you get |
|---|---|
| (default) | nothing on stdout/stderr unless something fails |
-s / --silent |
errors only, suppresses warnings too |
-v |
one info line per phase (read N bytes, wrote tempfile, invoke emacsclient) |
-vv |
adds the elisp form being sent |
--debug |
logfmt-formatted records: ts=… level=… component=epipe event=… key=value …. Independent of -v. |
EPIPE_DEBUG=1 |
env-flag equivalent of --debug |
Logfmt example:
ts=1761504123.456 level=info component=epipe event=start mode=file wait=true buffer="*pipe*" emacsclient=emacsclient
ts=1761504123.461 level=debug component=epipe event=stdin_read bytes=4096
ts=1761504123.462 level=debug component=epipe event=tempfile_created path=/tmp/epipe-1761504123461-abc12345 bytes=4096
ts=1761504123.475 level=info component=epipe event=invoke_emacsclient wait=true mode=file
ts=1761504123.482 level=info component=epipe event=done exit=0
Default is epipe-{ts}-{rand}. Substitute any of:
| Token | Value |
|---|---|
{hash} |
md5 of the input bytes (deterministic dedup if you want it) |
{pid} |
epipe PID |
{ts} |
unix epoch milliseconds |
{date} |
UTC YYYYMMDD-HHMMSS |
{rand} |
8-char random hex |
{user} |
$USER |
echo x | epipe --pattern "snip-{date}-{user}-{hash}.txt" --keep| Var | Purpose |
|---|---|
EMACSCLIENT |
Override path to emacsclient |
TMPDIR |
Default temp directory |
EPIPE_MODE |
Default mode (file/eval/inline) |
EPIPE_BUFFER |
Default buffer name |
EPIPE_DEBUG=1 |
Force --debug |
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Generic IO/preflight failure |
2 |
Bad arguments / refused input (e.g. stdin is a tty) |
127 |
emacsclient not found on $PATH |
| anything else | Forwarded from emacsclient (only in --wait mode) |
just build # cargo build --release
just test-rust # cargo test
just test-bash # bats parity suite vs bash impl
just test-rust-parity # bats parity suite vs rust impl
just test # all of the above
just verify # fmt + clippy + shellcheck + testA single bats suite (tests/shared.bats, 41 tests) runs against either implementation by setting EPIPE_BIN. A fake emacsclient (tests/fixtures/fake-emacsclient.bash) records every invocation -- argv, cwd, stdin -- to a record file the tests then assert on. This is what guarantees parity: both binaries must produce the exact same emacsclient invocations for the same inputs.
tests/
├── shared.bats -- 41 parity tests, shared across both impls
└── fixtures/
├── fake-emacsclient.bash -- records argv + stdin to $EPIPE_RECORD_FILE
└── helpers.bash -- setup/teardown, last_args(), invocation_count()
.
├── Cargo.toml
├── Justfile
├── readme.md
├── src/main.rs -- Rust impl (single file)
├── bash/epipe.bash -- Bash 5.3+ impl
├── tests/ -- shared parity suite
└── .github/workflows/ci.yml -- cargo build/test + bats on Linux
MIT.