Pipe content between your terminal and Emacs buffers - seamlessly bridge your command-line workflows with your editor.
mxp is a shell script that acts as a bridge between Unix pipes and Emacs buffers. It supports:
- Write mode: Pipe stdin to Emacs buffers (creates new or appends to existing)
- Read mode: Output buffer content to stdout for piping
- Open mode: Open files and directories (any number of them) in your running Emacs
- Streaming: Real-time content updates with chunked processing
- Buffer matching: Use exact names or regex patterns to find buffers
- Auto-generation: Creates
*Piper 1*,*Piper 2*, etc. when no name specified - Conflict handling: Avoids overwriting buffers unless forced
- Persistent socket: Auto-boots a TCP eval server inside Emacs for fast, ordered communication (falls back to
emacsclienttransparently)
The script works with both bash and zsh, handles special characters properly, and includes comprehensive error handling.
Howard Abram’s emacs-piper project although has similarities, it works differently and has different goals.
- Pipe command output directly into Emacs buffers for comfortable viewing
- Stream logs in real-time to watch them in your editor
- Extract buffer content from Emacs and pipe it to terminal commands
Emacs has piping in/out buffers in Eshell! You can pipe content into a buffer like this:
;; In eshell, you can pipe command output to a buffer
;; You can press <C-c M-b> to pick a buffer instead of typing it
cat file.txt > #<buffer some-buffer> Unfortunately, Eshell doesn’t support input redirection, so reading from a buffer ain’t so straightforward, yet still possible. You just need to create a custom eshell command to read from a buffer, e.g.:
;; note the eshell/ prefix
(defun eshell/b (buf-or-regexp)
"Output buffer content of buffer matching BUF-OR-REGEXP."
(let ((buf (if (bufferp buf-or-regexp)
buf-or-regexp
(cl-loop for b in (buffer-list)
thereis (and (string-match-p
buf-or-regexp (buffer-name b))
b)))))
(when buf
(with-current-buffer buf
(buffer-substring-no-properties (point-min) (point-max))))))And then we can use it in Eshell:
;; Press <C-c M-b> to pick a buffer instead of typing it
b #<buffer README.org> | rg "error"Incredibly powerful, but this only works inside Eshell - you cannot use it in an external terminal. mxp brings this same workflow to your regular shell, letting you pipe between any terminal and Emacs.
- Emacs with
emacsclient(Emacs daemon must be running) - bash >= 4.0 or zsh
- Standard Unix utilities (
base64,grep,sed)
Download and put it somewhere in the $PATH
curl -fsSL https://raw.githubusercontent.com/agzam/mxp/refs/heads/main/mxp -o \
~/.local/bin/mxp \
&& chmod +x ~/.local/bin/mxp Make sure Emacs daemon is running and emacsclient works!
On first use, mxp boots a lightweight TCP eval server inside your running Emacs and connects to it directly via bash’s /dev/tcp. All subsequent calls reuse this persistent connection, which means:
- No process spawning per eval (previously each chunk launched a new
emacsclientprocess) - Guaranteed ordering for streamed content (no more background job races)
- No “Connection refused” errors under heavy streaming load
The server starts automatically - no manual M-x step required. If the socket is unavailable (e.g., bash built without /dev/tcp support), mxp falls back to emacsclient transparently.
| Variable | Default | Description |
|---|---|---|
MXP_PORT | 17394 | TCP port for the eval server (localhost only) |
MXP_NO_SOCKET | (unset) | Set to 1 to force emacsclient-only mode |
EMACS_SOCKET_NAME | (unset) | Named socket for emacsclient -s (e.g., daemon name) |
You can also pass -s / --socket-name on the command line:
echo "hello" | mxp -s myserver "*my-buffer*"
mxp --socket-name myserver --from "*my-buffer*"This is useful when the Emacs daemon was started with a specific socket name, e.g., emacs --daemon=myserver.
To stop the server manually:
(when (and (boundp 'mxp-server-process) (process-live-p mxp-server-process))
(delete-process mxp-server-process)
(setq mxp-server-process nil))# Open a file
mxp my-file.txt
mxp README.org
# Open current directory in dired
mxp .
# Open any directory
mxp ~/Projects
mxp /path/to/directory
# Works with relative paths
mxp ../other-project/file.txt
# Open several things at once (a single emacsclient call)
mxp --open notes.org "file with spaces.org" src/Open mode works the same when stdin is not a terminal, which makes it composable with xargs and find. Every file lands in your one running Emacs - unlike xargs emacs, which spawns a fresh instance per batch and trips over filenames with spaces:
# Open every file that matches a search
rg -l0 "TODO" -g '*.org' | xargs -0 mxp --open
# Same via find
find . -name '*.org' -exec mxp --open {} +How the mode is chosen: explicit flags (-o / -r / -w / -f) always win; otherwise arguments that all exist on the filesystem are opened, piped stdin or not; otherwise, on a terminal, path-shaped arguments (starting with a slash, tilde or dot, or containing a slash) are opened while a bare word reads that buffer; otherwise piped stdin writes to the buffer, so regex buffer names like ".*log.*" keep working in pipes. To pipe into a buffer whose name collides with an existing file, be explicit: cat data | mxp -w my-notes.
Open mode is silent on success. Missing paths are reported to stderr, the remaining ones still open, and the exit code is nonzero.
# Pipe to a named buffer
cat file.txt | mxp "my-buffer"
# Pipe to auto-generated buffer (*Piper 1*, *Piper 2*, etc.)
tail -f /var/log/app.log | mxp
# Append to existing buffer
echo "more content" | mxp --append "my-buffer"
echo "more content" | mxp -a "my-buffer"
# Prepend to existing buffer (insert at the top)
echo "header info" | mxp --prepend "my-buffer"
echo "header info" | mxp -p "my-buffer"
# Match buffer by regex
echo "data" | mxp "mybuf.*"
# Force overwrite existing buffer
cat new.txt | mxp --force "my-buffer"
cat new.txt | mxp -F "my-buffer"# Output buffer to stdout
mxp --from "my-buffer"
mxp -f "my-buffer"
# Pipe buffer to commands
mxp --from "*Messages*" | grep error
mxp -f ".*scratch.*" | wc -l
# Use in command chains
mxp -f "my-buffer" | sort | uniq | lessWorks naturally with process substitution for commands expecting files:
# Compare two buffers
diff <(mxp -f "version-1") <(mxp -f "version-2")
# Use buffer as input file
jq . <(mxp -f "*json-data*")There are hooks that you can customize:
| Runs | Args | Notes | |
|---|---|---|---|
mxp-buffer-hook | when the buffer appears | BUFFER-NAME | useful for setting major mode, etc. |
mxp-buffer-update-hook | whenever there’s more data | BUFFER-NAMEBEG-POS,END-POSwhere buffer gets updated | |
mxp-buffer-complete-hook | at the completion | BUFFER-NAME | may never run for continuous streams |
Hook examples:
(defun on-mxp-buffer-h (buffer-name)
(with-current-buffer buffer-name
(when (string-match ".*\\.json.*" buffer-name)
(json-mode))))
(add-hook 'mxp-buffer-hook #'on-mxp-buffer-h)
;; This is how you can re-apply colors. I don't want to make escape
;; color code processing built into the script itself. It's better to
;; keep that customizable.
(defun on-mxp-buffer-update-h (buffer-name beg end)
(with-current-buffer buffer-name
(ansi-color-apply-on-region beg end)))
(add-hook 'mxp-buffer-update-hook #'on-mxp-buffer-update-h)
(defun on-mxp-buffer-complete-h (buffer-name)
(with-current-buffer buffer-name
;; delete all empty lines
(flush-lines "^$" (point-min) (point-max))))
(add-hook 'mxp-buffer-complete-hook #'on-mxp-buffer-complete-h)# Quick file/directory access
mxp config.json
mxp .
mxp ~/Documents
mxp $HOME
# Watch build logs in Emacs
npm run build | mxp "build-logs"
# Send curl output to Emacs for inspection
curl -s "https://api.thedogapi.com/v1/breeds" | jq | mxp "breeds"
# and the the opposite direction:
mxp "breeds" | jq '.[].name' | sort | mxp "dog names"
# Extract TODO items from buffer
mxp -f "*scratch*" | grep TODO > todos.txt
# Add timestamps to the top of a log buffer
date | mxp --prepend "logs"
tail -f app.log | mxp --append "logs"
# Stitch multiple buffers together
cat <(mxp -f "header") <(mxp -f "body") | mail -s "Report" user@example.com
# Edit a file, then pipe its buffer content through a command
mxp config.yaml # Opens in Emacs
mxp -f config.yaml | yq '.version' - # Read it back
# Stream some data with a passtrhough (shows results in both the buffer and terminal)
ping google.com | tee >(mxp)