Skip to main content
  1. posts/

Ghostty xterm-ghostty Over SSH: What's Actually Fixed (and What Isn't)

 Author
Author
Christian Mohn
IT veteran, podcaster, author, and blogger from Bergen, Norway.
Table of Contents

My Ghostty workaround post from December 2024 has an update box at the bottom pointing to Ghostty 1.2.0’s ssh-env/ssh-terminfo shell integration, which was supposed to make the whole workaround unnecessary.

I went back to test that claim properly, because I’m still hitting problems with it in my own setup. So I dug into what’s actually shipped, what’s still open on Ghostty’s own issue tracker, and what the terminfo situation upstream actually looks like. The short version: there isn’t one fix here, there are two, and only one of them has shipped.

Everything hands-on here, the Homebrew paths, the tip install, is from my own Mac. If you’re on Linux the broad strokes should hold, but the exact commands and package names will differ, and I haven’t tested it there myself.

Two different fixes, only one of them is out
#

The ssh-env/ssh-terminfo shell integration flags (shell-integration-features = ssh-env,ssh-terminfo in your Ghostty config) shipped in 1.2.0, back in September 2025, and are what current stable 1.3.1 has. They’re shell functions that wrap your interactive ssh calls, setting $TERM and copying the terminfo entry over.

There’s also a ghostty +ssh subcommand in the works, a proper wrapper binary rather than a shell function, which sidesteps a bunch of the shell-function limitations described below. Ghostty’s own docs are explicit about where that stands right now:

+ssh requires Ghostty tip [pre-release], which will become Ghostty 1.4.0. It is not present in Ghostty 1.3.x.

1.4.0 hasn’t shipped, but you don’t have to wait for it to try +ssh. Homebrew has a ghostty@tip cask that tracks Ghostty’s main branch as a prebuilt nightly, no Zig or Xcode required to build it yourself. I installed it (brew uninstall --cask ghostty && brew install --cask ghostty@tip, since tip and stable both install to the same /Applications/Ghostty.app and can’t run side by side) and ghostty +ssh --help is right there, working, on 1.3.2-main.

Worth being honest about what tip actually is here: it’s an unreleased, unstabilized build of main, not a beta with any real testing pass behind it. It replaces your daily driver terminal rather than running alongside it, and there’s no guarantee it won’t regress something else while you’re on it. I wouldn’t put it on a machine you can’t afford to fuss with, and I’d keep the stable cask’s Caskroom backup around so switching back is a one-liner if something breaks.

If you’re on stable Ghostty, you only have the shell-function version, not the wrapper. That’s most of why I was still seeing problems: 1.2.0’s fix and the fix I actually needed turned out to be two different things.

Where the shell-function version falls short
#

Because ssh-env and ssh-terminfo are shell functions, not a wrapper around the actual ssh binary, anything that calls ssh without going through your interactive shell skips them entirely. That includes cron jobs, Makefiles, aws ... ssh, gcloud compute ssh, mosh, rsync -e ssh, and git or scp over SSH. None of those get the terminfo fix, because none of them run through the shell function that applies it.

A few other rough edges, straight from Ghostty’s own issue tracker and discussions:

  • It’s flatly incompatible with SSH config’s RemoteCommand directive. A frequent contributor confirmed the behavior and recommended disabling the feature and installing terminfo manually instead if you rely on RemoteCommand.
  • ssh-env unconditionally overwrites $TERM to xterm-256color, with no way to opt out, even if you’ve customized Ghostty’s own term setting.
  • There have been real bugs in the caching mechanism itself: the terminfo cache silently failing when a state directory doesn’t exist, failing across filesystems when $TMPDIR and $XDG_STATE_HOME are on different mounts, and IPv6 addresses never caching correctly so the terminfo gets reinstalled on every single connection. These are all fixed now, but they tell you this is still a young, actively-patched mechanism, not a solved problem.

The part that surprised me: the terminfo name doesn’t even match
#

Here’s the thing that actually explains why this keeps being a problem no matter which Ghostty version you’re on. Upstream ncurses did add a Ghostty terminfo entry, back in December 2024, only a day after someone submitted it to the bug-ncurses mailing list.

The maintainer named the entry ghostty. Not xterm-ghostty, which is what Ghostty actually sends as $TERM by default. He disputed Ghostty’s naming choice directly on the mailing list. Ghostty hasn’t changed what it sends.

So even a fully up to date Linux box with current ncurses does not recognize what Ghostty sends over SSH, because the names don’t match. This isn’t theoretical. There’s a live report from someone on Fedora 44 with ncurses 6.6, current as of this year, still getting no terminfo match at all and having to symlink it manually.

macOS has its own separate, worse version of this problem: the ncurses Apple ships is ancient, can’t read a modern terminfo entry format at all, and /usr/share/terminfo has been read-only since Catalina. Ghostty’s own docs recommend switching to Homebrew’s ncurses for macOS versions before Sonoma, and give the actual fix:

brew install ncurses

Then use Homebrew’s infocmp instead of the system one when copying terminfo to a remote host, since the system version is too old to read what it produces and throws an “Illegal character” error if you try:

/opt/homebrew/opt/ncurses/bin/infocmp -x xterm-ghostty | ssh YOUR-SERVER -- tic -x -

(swap in /usr/local/opt/ncurses/bin/infocmp on Intel Macs).

None of this applies if you’re on tip using +ssh. It ships its own terminfo definition compiled into the Ghostty binary and never touches your local infocmp, so the old macOS ncurses is never in the picture. This is only relevant for the manual one-liner from my original post, or if you’re stuck on the older shell-function mechanism.

The naming mismatch itself isn’t something any of these mechanisms fix at the root, ssh-env, ssh-terminfo, or +ssh alike. They all work around a missing or wrong terminfo entry on the remote side. None of them make the two projects agree on a name.

What actually breaks, and what doesn’t
#

My original post said this produces a real functional error, not just wrong colors, and that holds up. What I didn’t do the first time was separate out which tools actually fail versus which ones just complain:

  • htop refuses to start: Error opening terminal: xterm-ghostty.
  • tmux refuses to create a session with a “missing or unsuitable terminal” error.
  • nano won’t open at all.
  • tput, clear, and mcedit fail outright or behave incorrectly.
  • vim is the exception. It has its own internal minimal termcap fallback, so it degrades with a warning instead of refusing to run.

That’s a real, hard failure for most full-screen terminal tools, and it’s the reason this keeps being worth watching instead of shrugging it off as a cosmetic quirk.

Where that leaves things today
#

If you’re on Ghostty stable right now and still hitting missing or unsuitable terminal errors, you’re not doing anything wrong. You have the older, more limited fix, it has real documented gaps, and the more complete wrapper isn’t released yet. My original .zshrc workaround still works reliably today, because it doesn’t depend on any of this. It has the same limitation as ssh-env/ssh-terminfo though: it only applies in an interactive shell, so cron jobs, Makefiles, and anything else that doesn’t source .zshrc won’t get it either.

+ssh closes the non-interactive-shell gap for real: it execs the real ssh binary itself once it’s done, so it works as a drop-in replacement anywhere ssh is called, scripts and cron included, not just in your interactive shell. It also doesn’t touch your local infocmp at all. It ships its own terminfo definition compiled directly into the Ghostty binary and sends that to the remote host, which means the macOS ncurses problem above doesn’t apply to it either.

What it still won’t fix is the xterm-ghostty versus ghostty naming mismatch upstream, because that’s not Ghostty’s code to fix. That part needs the two projects to actually agree on a name, and as of today, they haven’t.

Related