Skip to content

The setlocale Warning on Every SSH Login Comes From Your Laptop

Adityo Guni Waluyo

Every SSH login prints setlocale: LC_CTYPE cannot change locale. The sender is your own Debian laptop via its SendEnv default.

Two setlocale Lines Under the Login Banner

This morning I SSH'd into a brand-new VPS. The MOTD banner appeared, uptime tidy, provider welcome text. Then right under it, two uninvited lines:

setlocale: LC_CTYPE cannot change locale
setlocale: LC_CTYPE cannot change locale

And they came back every time I opened a new shell. My first reflex: the server's locale is broken. I ran locale on the server: LANG=C.UTF-8 plus a row of empty LC_*. locale -a only listed C, C.UTF-8, and POSIX. So the locale being requested genuinely doesn't exist on the server.

My first instinct was to regenerate it with locale-gen. Good thing I held off. Minimal images ship with few locales by design; that's not breakage. The server wasn't asking for anything. The noisy one was my own Debian laptop: on every connect, my SSH client ships LANG and every LC_* variable to the server, and the shell there tries to comply.

SendEnv Is Additive, Not First-Value-Wins

Debian's stock SSH client carries this default in /etc/ssh/ssh_config [2]:

SendEnv LANG LC_*

So on every connection the laptop sends LANG plus everything matching the LC_* pattern. Most Debian servers also have AcceptEnv LANG LC_* in sshd_config [3], so the variables get passed into my shell. The shell calls setlocale, the server's libc has never heard of that locale, and out come those two lines. With no matching AcceptEnv, sent variables are silently dropped; sshd's default is to accept no environment variables at all [3].

I had already tried disabling it from ~/.ssh/config, and it still got sent. That's where my misunderstanding lived. Config read order: command-line options, then ~/.ssh/config, then /etc/ssh/ssh_config plus the /etc/ssh/ssh_config.d/*.conf files included at its top [1][2]. For regular directives, first-obtained-value-wins: the first value seen wins, which is why specific declarations go near the top. I assumed SendEnv worked the same way.

It doesn't. SendEnv is additive: each occurrence adds names to the same list, it doesn't override. Names may be wildcards, and a - prefix removes entries from the list [1]. The "a block at the bottom doesn't win" logic doesn't apply here. How negation interacts across files is subtle, so instead of debating theory I just verified the effective result.

Fix It at the Sender, Check With ssh -G

In ~/.ssh/config:

Host vps-baru
    HostName 203.0.113.10
    SendEnv -LANG -LC_*

If you still want to send variables that are safe on any server, add a second line: SendEnv COLORTERM NO_COLOR. Neither makes setlocale on the server complain.

Check the effective config without connecting:

ssh -G vps-baru | grep -i sendenv

ssh -G dumps every option that would apply to that host, after all config files are merged. If sendenv LANG LC_* still shows up there, the system config entry is still hanging in the list. The laziest fix that always works on your own laptop: comment out the SendEnv LANG LC_* line in /etc/ssh/ssh_config. It's your machine, nobody will object.

Log in again: clean banner, warnings gone. One small note: TERM is still sent whenever a pty is requested; that's part of the SSH protocol, it can't and shouldn't be blocked [1].

When locale-gen on the Server Still Makes Sense

If the server genuinely needs that locale, not just to please the sending laptop, then generate it server-side: uncomment the matching line in /etc/locale.gen, then run locale-gen or dpkg-reconfigure locales [4]. Cases where that's reasonable: an app on the server depends on a specific language collation, a cron job formats dates locally, or other users on the box need it. The Debian wiki itself recommends a default locale of None for hosts accessed over SSH [4].

My honest opinion: minimal images are designed to stay lean, so don't force a locale generation just to silence a warning on your laptop. Variables sent uninvited get pulled back at the sender; the server shouldn't carry the burden. And the takeaway: SendEnv doesn't follow first-value-wins, and ssh -G is the cheapest way to stop guessing.

Sources

[1] https://man.openbsd.org/ssh_config [2] https://manpages.debian.org/bookworm/openssh-client/ssh_config.5.en.html [3] https://man.openbsd.org/sshd_config [4] https://wiki.debian.org/Locale

Sources

[1] https://man.openbsd.org/ssh_config [2] https://manpages.debian.org/bookworm/openssh-client/ssh_config.5.en.html [3] https://man.openbsd.org/sshd_config [4] https://wiki.debian.org/Locale