PortButler — Native SSH, SFTP and serial for macOS

16 min read Original article ↗

macOS · SSH · SFTP · Serial

Three of them.
One window.

A terminal, a file manager and a serial monitor — three apps for one machine. Here a host is a tab, its files are the pane beside it, and the board on your desk sits in the same list as the servers.

No account, no card, nothing to enter · macOS 13+ · Apple silicon and Intel · Notarized by Apple

Sessions, two file panes and a terminal in one PortButler window
One window. Hosts on the left, their files in the middle, a shell on the right — and the serial adapter sits in the same list as the servers.

When it goes wrong

It tells you why it failed.

connection failed is not an error message. These five look identical on most clients and each needs a completely different fix.

What happenedWhat PortButler tells you
The port refused itThe machine is up. Check the port — many devices use 2222
Nothing answeredPowered off, or a firewall dropping packets silently
The name will not resolveCheck spelling, or whether it only exists on a VPN
Connected, no SSH greetingThat port is not SSH, or sshd is stuck
Something answered, not SSHA web server may be sitting on that port

The third one

The board on your desk is in the same list.

A USB-serial adapter shows up beside your servers, not in another app. Speed and framing are picked when you connect, and /dev/cu.* is found for you — minus the two every Mac has and nobody wants to see.

Every line can carry the time it arrived. Watching a board boot is most of what a serial port is for, and "which step is slow" is unanswerable without it:

[12:34:56.789] Booting Linux on physical CPU 0x0
[12:34:56.812] Linux version 6.6.31-v8+
[12:34:57.104] Machine model: Raspberry Pi 5
[12:35:01.882] mmc0: error -110 whilst initialising SD card
[12:35:04.377] systemd[1]: Reached target Multi-User System

There are 4.8 seconds between the last two lines. That is the SD card timing out and the kernel waiting for it — the single most useful fact in that log, and it is invisible without the clock. Every other line looks the same either way.

And a pasted config block arrives whole. A switch at 9600 with no flow control drops whatever overruns its input buffer, silently: half your configuration goes in, the device says nothing, and you find out later from behaviour rather than from an error. PortButler can pace the paste, per line or per character. Your bytes are not touched — no \n\r rewriting, same order — so with the delay at zero the device receives exactly what it received before. Saved snippets go out through the same paced path, so a long block never arrives half-eaten.

Milliseconds, because a boot log throws several lines into the same second. A partial line gets one stamp, not one per fragment — serial always arrives in pieces. And a \r is not a new line, so a progress bar redrawing itself does not fill your screen with clocks.

Files down a console cable

A serial line has no idea what a file is.

It is a wire that carries bytes in a row. Nothing in it says this is a file, this is where it ends, or I got that. To move a file, both ends have to agree on a convention beforehand — and both ends have to know it. We know all of them. The question is always what is on the far side.

MethodNeeds on the deviceYou type thereWhen
XMODEMlrzszrx Very old equipment. 128-byte blocks
XMODEM-1Klrzszrx The same, eight times fewer round trips
YMODEMlrzsz or U-Bootrb Carries the name and size. U-Boot calls it loady
ZMODEMlrzszrz Fastest on Linux. U-Boot cannot do it
Textnothingnothing When none of the above is available

lrzsz is the package that teaches a Linux box those conventions. The names follow a rule worth knowing: r receives and s sends, and the last letter is the method — b for YMODEM, z for ZMODEM, x for XMODEM. Once you see that, you can pick the command yourself.

Which way, and where it lands

DirectionWhere the file ends up
SendMac → device The device's current directory — no path is attached
Receivedevice → Mac It asks. The default is Settings ▸ Files ▸ download location

Yes, you can pull files back. The cable has a TX and an RX wire, so it goes both ways — and the reason you are on a console cable in the first place is usually that the box has no network. When that is true, the console is the only way out for a log, a config backup, or a crash dump.

Who speaks first — it depends on the method

The three behave differently here, and it changes what you do:

XMODEM and YMODEM — the device speaks first. It sends the letter C over and over, meaning ready, and our end writes nothing at all until it sees one. So press Send whenever you like: it waits, and nothing is dumped into your console while it does. The green dot lights when that C arrives.

ZMODEM — we speak first. Press Send and the app types rz on the device for you, then begins. The device still needs lrzsz installed — if it is not there you will see -sh: rz: command not found and a burst of garbage, which is the receiver talking to nobody.

Text — nobody needs to speak first, because there is no convention to agree on. The app types every line itself.

When the device has none of it

This is the one that is ours. The app turns the file into base64 text and types five lines for you — open a file on the device, paste the encoded bytes, close it, decode it, and compare the hash against the one measured here:

cat > /tmp/firmware.b64
…base64 text…
^D
base64 -d /tmp/firmware.b64 > firmware && rm /tmp/firmware.b64
sha256sum firmware      ← compared with the value measured on the Mac

It needs only base64 and sha256sum, which are on essentially every Linux. The encoding is what makes it safe for real files: compressed data and firmware always contain bytes like 0x03 and 0x11, which a console reads as Ctrl-C and XOFF rather than as data. Base64 uses only 64 harmless characters, so a .tar.gz or a .bin goes through intact.

A file being sent to a device over serial as base64, with the hash verified on arrival
Nothing was installed on that device, and nothing was typed on it. The app wrote every one of those lines, then compared the hash.

The hash check cannot be switched off. A device without flow control drops whatever overruns its buffer and says nothing, which leaves a file of exactly the right length with the wrong bytes inside. On firmware that is a brick.

Around 8 KB/s at 115200 — roughly two minutes per megabyte. That is arithmetic from the line rate and base64's one-third overhead, not a bench measurement. Either way the shape is right: generous for a log, a config or a small firmware image, and wrong for a root filesystem. Worth knowing before you start rather than after.

One thing to know: pressing a key during a paste or a text transfer stops it. That is deliberate — it keeps a stray keystroke from landing in the middle of a config line — but on a transfer that runs for minutes, one accidental key ends it, and a partial file is left on the device. The app tells you when that happens.

Where the device forgets

A console forgets. The terminal does not.

Some consoles remember nothing at all — a bootloader, a bare MCU prompt, a stripped BusyBox build. Others remember a little and then throw it away: Cisco IOS keeps ten lines by default, and they die with the session. Pull the cable, come back after lunch, and it is gone. None of it is on your Mac, none of it is searchable, and none of it survives the device being power-cycled — which, on a bench, it will be.

So an engineer types show interface gigabitethernet 0/1 by hand, forty times a day.

PortButler remembers the lines you have typed on that port and offers the rest of one in grey, in front of the cursor. → accepts it. Nothing else changes — with no suggestion showing, → is just → and goes to the device like any other key. It switches itself off inside vim and less, where a phantom character would be worse than useless.

This is not a shell plugin like zsh-autosuggestions. Those run inside the shell, which is fine when there is a shell to put one in — and on a switch, a bootloader or an MCU there is not. Ours runs on this side of the cable, so what it remembers is yours, on your Mac, per host, and still there next week. Over SSH you may not need it: the shell you land in often does this already, and better. That is why you can switch it off for a host.

We have not found another terminal that does this on a serial port. That is not the same as there being none — we looked, we did not survey. If you know one, tell us and this paragraph changes.

Tab stays with the device, on purpose. Every inline-suggestion tool works this way — zsh-autosuggestions, fish, Warp, Amazon Q all accept with → and leave Tab to completion, and Warp's own documentation admits that binding Tab to suggestions means moving the completion menu somewhere else. We could not make that trade even if we wanted to: the Tab we would be taking is not ours, it belongs to your Cisco IOS, your U-Boot, your bash.

And the two are not the same thing. We only know what you typed before. Tab knows what is there now — the file you made five minutes ago, the branch you just created, the directory that exists only on that box. Taking Tab would trade the better one for the worse one.

Passwords are never remembered, and two separate rules make sure of it. A password prompt is caught because the console stops echoing what you type. But snmp-server community S3cr3t is echoed — it looks like an ordinary command — so a second rule cuts the line at the word the secret follows. One rule alone would leak; that is why there are two. You can switch the memory off per port, and clear what it holds.

New

A tunnel is a session, not a side effect.

Everywhere else a port forward is a checkbox on a host, so it dies when you close the terminal it was riding on. Here it is its own thing in the list, next to your servers. Open a port for the afternoon with no terminal attached.

It does not ask you for the address, the account or the key a second time — it points at a host you already saved. Change that password next month and the tunnel follows.

StateLocal portGoes toOpen nowMoved
open5163workstation:5163 23.2 MB
blockedsocks 1080whatever connects

Watch the last column. A tunnel that is open and idle looks exactly like one that is quietly broken — both are a green dot. Other clients show you that dot and stop there. The bytes are how you tell the difference, and they are the reason this screen exists.

Measured, not quoted

108 MB/s, where scp gets 74.

Same Mac, same server, same 64 MB file. The reason is not micro-optimisation — it is the shape of the transfer. scp waits for each 32 KiB chunk to be acknowledged before asking for the next. PortButler keeps four streams in flight and writes each 256 KiB chunk in place.

DownloadRaspberry Pi 5 over 1GbESame machine (no wire limit)
PortButler108 MB/s465 MB/s
scp74 MB/s267 MB/s
sftp74 MB/s267 MB/s

Uploads are not faster. They match scp — we measure 74 MB/s over the same gigabit link. Reading a file is where several requests can be in the air at once; writing one is not, and writing to several places in a remote file at the same time leaves a hole-ridden file if the transfer is cut off. We would rather be slow than hand you a firmware image with a gap in it. The same page lists the other conditions where the gap closes — small files, a server whose CPU gives out first, a wire that is already full.

And when a big one dies at 90%, it picks up from there. A dropped Wi-Fi or a closed lid leaves a marked partial file, and the next attempt asks whether to resume it — never silently, in either direction. Parallel transfers make this harder than it sounds: four streams write to different offsets, so an interrupted piece has holes in it. Resuming from the file's size would append past those zeroed gaps and hand you a corrupt image. PortButler keeps only the part that is genuinely there.

And if the app is killed outright, the leftover piece is not trusted at all — nothing recorded how far it actually got, so the next attempt starts over rather than guess. You lose the progress, not the file. On a firmware image that is the only acceptable trade: a download that has to start again is an annoyance, and one that silently completes wrong is a brick.

The number is on screen while it runs. Megabytes per second and the time left, in the same units as the table — so you are not asked to take this page's word for it.

A transfer in progress showing megabytes per second and the time left

That 108 MB/s is 91% of the theoretical maximum of a gigabit wire. There is no room left to win on that cable, which is why the same-machine column exists. Every number here was measured on one Mac, and the method is published — BENCHMARK, in English, with the commands.

Built for fleets

Five thousand boards, one password.

That is what an embedded rollout looks like: the same account and the same password on every unit. Save it once as a credential and type an address — the login comes from there. Change the password later and every machine follows, because they point at the credential, not a copy of it.

The password never appears

The list shows names and accounts. The password lives in this Mac's Keychain, is read the instant you press Return, and goes straight to the session — it is not rendered, and it never leaves this device.

It will not lock you out

Servers count failed logins. Most clients offer every key in your agent and get cut off before you are ever asked for a password. PortButler offers at most four, so an attempt is always left — and when the credential says password, it offers none.

Answering a sudo password prompt from a saved credential
A password prompt appears, the list opens over it, type to filter, Return to send.

Before you buy

What it does not do.

Listed first, because you will find out anyway and it is better you find out here.

PortButlerWhere it exists
Folder transfer and a queueNot yetTransmit, ForkLift
Same input to many sessionsNot yetSecureCRT, iTerm
Split panesTabs onlyiTerm, WindTerm
Serial in the same windowYesWindTerm, Termius
Tunnel that outlives the terminalYesTermius too
Live throughput on that tunnelYesWe could not find it elsewhere
Saved command snippetsYesTermius, WindTerm

Nothing on that list is a surprise waiting for you. The first two are being built, in that order. Everything else here is what you get today, and you have fourteen days to find out whether the gaps matter for your work — before any money changes hands.

It reads ~/.ssh/known_hosts and uses ssh-agent and the keys you already have. It imports ~/.ssh/config when you ask it to, and never writes to that file.

Where this is going

Three things that will not change.

No account. There is nothing to sign in to, so there is nothing to lock you out of. No telemetry. And your keys stay on this Mac — not as a policy we could revise, but because we run no server for them to go to. When a competitor moves your keys to their cloud on the paid tier, that is a thing we structurally cannot do.

You pay once. If we stop, the copy you have keeps working — it is a signed app on your disk, not a licence server that has to answer.

Next: the cable carries more than text

The same RS-485 pair you are reading a console on is usually carrying Modbus frames the rest of the day. Today that means two apps — and a serial port admits only one at a time, so you close one to open the other, and close it again to get back. We are putting the decoder in the window you are already in: a view mode beside hex, not a second application. It listens by default. Writing to a live bus is something you turn on deliberately.

And then: agents get hands, but not a free hand

Terminals are growing AI that runs commands for you. We are not going to do that, and we said so before it was a crowd: a command that runs on production hardware without someone reading it first is how you lose an afternoon or a factory.

What we will do is the other half. PortButler already holds the connection, knows whether it is talking to a bootloader or an OS, cuts secrets out of what it remembers, and refuses to rename a file it has not verified. An agent has none of that and needs all of it. So the agent gets to reach your board through us — and you get to watch, and press the button.

Your agent can reach the board. It still cannot touch it without you.

No dates on any of this. A date on a roadmap is a promise, and the only promises on this page are the ones about what the app does today.

Pricing

Try it for 14 days.

$29one-time · not a subscription

$29 through December 31. From January 1 it is $39 — and that is where it stays. Saying so now means buying early is buying early, not catching a discount that quietly vanishes.

When the trial ends it stops connecting — your session list stays. Major upgrades are $15. A ten-seat pack is $240.

For scale: Panic charges $45 for file transfer alone and $99 for SSH alone. Termius is $120 a year, and moves your SSH keys onto their servers when you upgrade. Ours never leave this Mac, on any plan, because there are no plans.

Download and use it for fourteen days first. Nothing to enter, no card.

macOS 13+ · Apple silicon and Intel · Notarized · Direct download, not the Mac App Store

Something not working, or missing? Open an issue — it is read by the person who wrote the code. The app has Help → Report an Issue… too, which fills in your macOS and app version.