# Remote locale settings in agent command tests

An agent can run the same command with the same arguments against the same files and still get a different answer on a remote host. The missing input is often the process locale. It changes how common tools classify text, order names, format numbers, print dates, choose an encoding, and word their errors.

Treat locale as test data, not machine decoration. Pin it when a test expects a stable protocol, vary it when the code promises to handle human conventions, and record it whenever an agent crosses a process or SSH boundary. Otherwise a green local test can hide a remote parsing bug until the agent acts on the wrong row, amount, or date.

This is not an argument for forcing English everywhere. A machine interface and a human interface have different jobs. Stable machine output needs an explicit format and environment. User facing output deserves deliberate localization. Mixing the two is how a translated diagnostic ends up parsed as state, or a comma decimal quietly becomes the wrong value.

## The remote process owns the result

The effective locale belongs to the process that executes the command. Your laptop locale does not control a program running through SSH unless something deliberately forwards or sets those variables, and an interactive remote shell may not match a noninteractive command session.

POSIX.1-2024 defines the precedence clearly. A nonempty `LC_ALL` overrides every category. If it is absent, a category variable such as `LC_TIME` or `LC_COLLATE` wins for that category. `LANG` supplies the default for categories that remain unset. That hierarchy means `LANG=C` does nothing when an inherited `LC_ALL=de_DE.UTF-8` is still present.

OpenSSH adds another boundary. Its client manual says `SendEnv` selects local variables to send, but the server must accept them. The default client behavior sends none. `SetEnv` can request explicit values, subject to the same server acceptance. A developer may have `SendEnv LANG LC_*` in a personal SSH configuration while the agent's stateless SSH helper has no such configuration, or the reverse. Neither outcome is safe to assume.

Login startup files make the picture messier. A distribution can set `LANG` through PAM or system configuration. A user's profile can change it for an interactive login. A remote command often runs without the same startup path. A container launched by that command may introduce its own locale set, and a minimal image may not have the named locale installed at all.

The fix is to set the intended environment at the final execution point. Do not rely on forwarding when the command needs a stable locale. Put the assignment next to the tool:

```sh
ssh buildbox 'env LC_ALL=C.UTF-8 TZ=UTC command-to-test --format=plain'
```

This makes the test contract visible. It also fails in an informative place if `C.UTF-8` is unavailable, rather than silently inheriting whatever the host chose. When you cannot count on that locale name, probe supported locales during provisioning and use a documented fallback.

## Record the environment before interpreting output

A remote failure report needs the effective categories, encoding, time zone, tool identity, and raw bytes. Capturing only `LANG` is inadequate because `LC_ALL` or a category variable may override it. Capturing decoded text alone can erase the evidence of an encoding failure.

Use a small probe before the command under investigation:

```sh
env | LC_ALL=C sort | sed -n '/^LANG=/p;/^LC_/p;/^TZ=/p'
printf 'charmap='; locale charmap
printf 'decimal='; locale -k decimal_point 2>/dev/null || true
printf 'date='; date +'%Y-%m-%dT%H:%M:%S%z'
printf 'tool='; command -v sort
sort --version 2>/dev/null | sed -n '1p'
```

A typical Linux result might have this shape:

```text
LANG=de_DE.UTF-8
LC_NUMERIC=de_DE.UTF-8
TZ=Europe/Berlin
charmap=UTF-8
decimal=decimal_point="," 
date=2026-07-24T143105+0200
tool=/usr/bin/sort
sort (GNU coreutils) 9.5
```

Do not turn that sample into an expected value. The point is the field set. Some `locale` implementations format keyword output differently, and other tool implementations may not support `--version`. Capture exit status and standard error for every probe so an absent feature does not masquerade as an empty value.

For encoding bugs, preserve bytes before decoding. A harness can write standard output and standard error to separate files, calculate checksums, and then decode a copy with the declared encoding. Hex output around the first invalid byte is much more useful than a replacement character inserted by a logging layer.

Also record the exact command transport. `ssh host command`, `ssh host sh -lc command`, an interactive terminal, and a process launched through an agent tool are distinct execution paths. They can select different shells, startup files, pseudo terminals, and environment filters. If the failing path uses an agent action, reproduce that path instead of proving that a hand typed login behaves correctly.

This diagnostic bundle belongs in the test artifact whenever results diverge. It turns "remote sort is flaky" into a comparison of concrete inputs.

## Pin a locale for protocols, not for people

Use a fixed locale when command output feeds a parser, snapshot, diff, cache key, deployment decision, or another program. Use the requested human locale when the output is meant for a person. Those are separate interfaces even if one command currently produces both.

The common advice to set `LC_ALL=C` everywhere is popular because it makes many Unix tools predictable and exists on POSIX systems. It is wrong as a blanket rule. The `C` locale can imply an ASCII oriented character model, depending on the system and runtime, so a program that reads names such as `Málaga` may reject or mishandle bytes even though ordering becomes stable.

`C.UTF-8` combines simple collation with UTF-8 on many current Unix systems, which makes it a practical test locale. POSIX does not require that exact locale name, however. macOS, Linux distributions, containers, and language runtimes do not expose an identical locale catalog. `locale -a` shows what the host offers, and provisioned test images should state which name they guarantee.

There is another distinction worth keeping sharp: locale stability is not output format stability. Pinning `LC_ALL` does not promise that two versions of a tool print identical columns, spacing, warnings, or JSON fields. If a tool offers JSON, NUL delimiters, epoch seconds, or an explicit format string, choose that interface as well. Locale control removes one variable; it does not freeze the program.

A good wrapper clears possible overrides and adds only what it needs:

```sh
run_stable() {
  env -u LANGUAGE -u LC_COLLATE -u LC_CTYPE -u LC_MESSAGES \
      -u LC_MONETARY -u LC_NUMERIC -u LC_TIME \
      LC_ALL=C.UTF-8 TZ=UTC "$@"
}
run_stable sort input.txt
```

If portability includes systems whose `env` lacks `-u`, construct a minimal environment instead. Keep `PATH` explicit and preserve only required application variables. Do not copy the whole parent environment and then patch `LANG`; that leaves category overrides in place.

Tests for user facing behavior take the opposite approach. They should select a supported locale intentionally and assert the convention that matters. A German report test may expect a comma decimal and German month text. The parser behind that report should still exchange normalized numbers and dates internally.

## Dates need a format and a time zone

Locale and time zone cause different date failures. `LC_TIME` controls names and conventional representations. `TZ` controls which civil time corresponds to an instant. Pinning one does not pin the other.

GNU Coreutils warns that the output of `date` is not automatically safe to parse later. Its manual recommends a language independent format, a Gregorian representation, and an unambiguous zone such as UTC or `Z` for generated data. That advice matters more than a snapshot that happens to pass under English settings.

For a test protocol, choose an explicit representation:

```sh
env LC_ALL=C.UTF-8 TZ=UTC date +'%Y-%m-%dT%H:%M:%SZ'
```

The output shape is `2026-07-24T12:31:05Z`. If the test needs a fixed instant rather than the current clock, pass that instant through the tool's supported option or inject a clock into the application. Locale control cannot make time stand still.

Avoid `%c`, `%x`, `%X`, `%a`, and `%b` in data that another program parses. They intentionally ask for locale conventions or translated names. Numeric directives can still carry calendar details on some systems. The GNU manual documents locales that use alternative calendars for certain directives, so an apparently numeric year is not a universal promise unless the format and chosen locale define it.

Week numbers are another trap. Calendar year, ISO week based year, and local week conventions answer different questions near New Year's Day. State which one the business rule uses and test boundary dates. A locale pin will not correct a mistaken `%Y-%V` combination.

Human reports should format dates at the edge of the system. Keep the stored or transmitted instant in a stable form, then apply the reader's locale and zone for display. If an agent must compare timestamps from several hosts, have it request epoch values or RFC 3339 style strings with offsets. Do not ask it to infer whether `03/04/26` means March 4 or April 3.

A useful date matrix includes one locale with English month names, one with different month names, UTC, a zone with daylight saving transitions, and dates around both a clock change and a year boundary. The goal is not to enumerate the planet. It is to expose code that assumed the developer's conventions.

## Sorting must match the consumer

Text has no single natural order. Byte order, Unicode code point order, and language collation produce different sequences. Tests fail when they expect one and invoke another.

The GNU `sort` manual states that comparisons normally use the sequence selected by `LC_COLLATE`. It specifically recommends `LC_ALL=C` when a script requires the traditional order. The same manual notes that setting only `LC_COLLATE` is unsafe if `LC_ALL` overrides it or if the character categories use incompatible encodings.

Consider this fixture:

```text
Zebra
apple
zebra
Ångström
ábaco
```

A `C` style locale commonly orders by encoded bytes, with uppercase ASCII before lowercase ASCII and UTF-8 sequences outside ASCII later. A language locale may compare case or accents at different collation levels. Do not paste one guessed ordering into a cross platform article or test. Run the actual supported locale and assert the semantic property you need.

For a reproducible manifest or golden file, byte oriented order is usually right. Set `LC_ALL=C` if every path is restricted to the portable character set, or use a verified UTF-8 locale and define the ordering function in the program. For a list shown to Spanish, Swedish, or German readers, a binary order is poor behavior. Use a locale aware collation library with a pinned data version, because operating system collation data can change independently of your code.

Sorting and joining must share the same rules. GNU Coreutils tells users to run `sort` and `join` with consistent locales and options. A file sorted under one collation can appear unsorted to `join` under another, causing missed matches or diagnostics. The same applies to `comm`, duplicate removal, merge operations, and any pipeline that assumes adjacent equal values.

Prefer assertions that expose intent. If order is irrelevant, compare sets or maps instead of snapshotting incidental order. If byte order is the protocol, calculate it in the test and label it. If localized order is the feature, include accented, case variant, and punctuation fixtures that distinguish it from binary sorting.

An agent can make this worse by using an exploratory `sort` command and then treating the first line as the "smallest" or "next" item. Put the ordering rule in its action contract. "Select the first release by semantic version" is different from "select the first filename under the remote locale."

## Decimal commas break shell pipelines quietly

`LC_NUMERIC` defines the decimal point and grouping conventions used by locale aware functions and some command options. A value displayed as `1,25` can be correct for a person and invalid for a parser expecting `1.25`. Worse, a permissive parser may accept only the prefix and return `1` without a loud error.

GNU `sort -n` uses the locale's thousands separator and decimal point when it recognizes numeric prefixes. Python's `locale.format_string`, `locale.atof`, and related functions also follow `LC_NUMERIC`. Python's ordinary `float()` and many data formats do not. Passing text between those two families without a boundary creates a bug that appears only under certain settings.

Keep protocol numbers normalized. JSON numbers use a period, command flags usually document a fixed grammar, and database wire formats define their own representation. Format a comma or grouped number only for display, after computation and serialization are complete.

Test parsers with values that make silent truncation visible:

```text
0.5
1.25
1234.75
-0.125
```

Then run the same operation under a locale with a comma decimal. If the tool intentionally accepts localized input, provide equivalent comma fixtures and reject ambiguous grouping. If it promises a fixed grammar, set the locale for the command and assert that a comma input fails clearly.

Do not "fix" arbitrary output by replacing every comma with a period. A comma may separate fields, group thousands, or occur in text. Use a structured output mode or a parser that knows the declared locale. If the producer does not publish a grammar, treat its human output as unsuitable for automation.

Shell arithmetic adds another source of false confidence. The shell may use a fixed syntax while an invoked `awk`, `printf`, spreadsheet converter, or language runtime applies locale in selected operations. Test the whole pipeline under one environment rather than testing each command in your login shell.

Money needs even stricter handling. Store minor units or a decimal type with an explicit currency, then localize only the rendered value. An agent deciding whether an amount crosses a limit should receive the normalized numeric value, not scrape a report meant for a reader.

## Encoding errors begin before decoding

Locale can tell a process how to interpret byte sequences as characters. `LC_CTYPE` affects character classification and is commonly tied to a codeset. That matters to tools that split text, match character classes, change case, calculate display width, or convert between bytes and strings.

UTF-8 on both machines does not prove that every process uses UTF-8. A remote service may start in the `C` locale, a minimal container may lack generated locale data, or a language runtime may enable its own UTF-8 mode. Python's documentation is candid here: preferred encoding can be only a guess on some systems, and Python UTF-8 Mode can ignore the locale encoding for that query.

Decode explicitly at boundaries. When a command contract says UTF-8, read bytes and decode as UTF-8 with a strict error policy. Do not call a platform default decoder and hope. If arbitrary filenames are possible on Unix, remember that filenames are byte sequences at the operating system boundary; forcing them through ordinary text can lose information. Use the runtime's dedicated filesystem encoding and reversible error strategy where available.

Error replacement is useful for display but dangerous for decisions. Two distinct invalid byte strings can collapse to the same visible replacement character. A test should fail with the byte offset, preserve the original output, and show a short hex window. That evidence tells you whether the producer emitted legacy encoding, truncated a sequence, or returned binary data through a text channel.

Character classes deserve direct fixtures. Under different locales, `[[:alpha:]]`, case conversion, and whitespace recognition can include different characters. POSIX explains that `LC_CTYPE` determines how byte sequences become characters and which characters belong to classes. If a script sanitizes names with a locale sensitive range, it can accept or remove different text remotely.

Use Unicode aware application code for human text and explicit ASCII rules for protocol identifiers. Do not let the ambient locale decide what counts as an environment variable name, token, or wire field. Conversely, do not use an ASCII only filter on a person's name and call the result validation.

A compact encoding test set should include plain ASCII, precomposed accented text, the same visible text with combining marks, another writing system, and one deliberately invalid byte sequence where the interface permits raw bytes. Assert bytes at protocol boundaries and characters after decoding. That separation makes failures legible.

## A locale matrix catches assumptions without exploding the suite

Run most deterministic command tests under one pinned locale, then run a smaller variation suite that is chosen to break assumptions. Testing every installed locale wastes time and still gives weak coverage because many locales share the same relevant conventions.

Choose variants by behavior:

1. Use `C` for portable byte behavior and translated diagnostics that should not be parsed.
2. Use an available UTF-8 locale with a period decimal and nontrivial collation.
3. Use an available UTF-8 locale with a comma decimal and different date names.
4. Add a locale or runtime mode that exposes encoding assumptions if the product supports it.
5. Pair date cases with UTC and one daylight saving zone.

Provision those locales in the test image. A skipped locale test because the runner lacks locale data is not a pass. Print `locale -a` when setup fails, and make the required catalog part of the image definition.

Keep the matrix close to the process launch. In Python, pass a copied environment to `subprocess.run` rather than changing the global locale inside a threaded test runner:

```python
import os
import subprocess

def run_case(locale_name):
    child_env = os.environ.copy()
    child_env.update({"LC_ALL": locale_name, "TZ": "UTC"})
    return subprocess.run(
        ["./agent-command", "inspect", "fixtures/names.txt"],
        env=child_env,
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
```

Python's locale manual says `setlocale()` is not thread safe on most systems and changes a program wide property. Changing it around tests can make parallel cases affect one another. A child environment isolates the command being tested and mirrors how remote execution works.

Assertions should separate exit status, standard output bytes, standard error bytes, and parsed meaning. Diagnostic language may change under `LC_MESSAGES` without indicating a different failure. A test that matches the English phrase "No such file" is testing a translation catalog, not the error condition. Prefer exit codes, structured error fields, or stable identifiers.

When a variation fails, minimize by category. Start with `LC_ALL` cleared, then set `LANG` and individual categories to find whether time, collation, numeric formatting, messages, or character handling caused the change. This is one place where category variables make excellent diagnostic tools even if production uses one `LC_ALL` value.

Run the smaller matrix on pull requests that touch parsing, process launch, SSH, reporting, or fixtures. A scheduled run can cover more operating systems and tool versions. Save the environment probe beside every failure so a rerun does not depend on memory.

## Agent actions need an execution contract

An autonomous agent amplifies locale ambiguity because it can chain a plausible output into a consequential action. If a listing sorts differently, the agent may select another file. If a decimal parser truncates, it may compare the wrong limit. If a date moves across a zone boundary, it may act on the wrong day's record.

Give remote command tools an explicit contract with four parts: the environment they set, the bytes or structured data they return, the exit information they preserve, and the shell semantics they use. Include tool versions or capability probes when output varies across implementations. Prompts cannot repair an unspecified process boundary.

Define what success means before an agent sees the output. A zero exit status may mean the command completed, not that it found a record. Some tools report partial results with a warning, while others use standard error for progress even on success. Preserve all three channels, then let a parser with a stated grammar decide whether the result is usable. Do not ask a language model to infer success from the tone of a localized message.

Make unsupported locales a setup error rather than an execution surprise. If a command starts with `LC_ALL=fr_FR.UTF-8` on a host without that locale, the shell or runtime may warn and fall back, or the program may fail. The harness should first confirm the locale through `locale -a` or a runtime capability check, record the selected name, and stop when the required behavior cannot be exercised. A fallback chosen during provisioning is controlled; a fallback chosen halfway through an agent action is hidden state.

Interpreter layers also need separate review. The local process builds an SSH argument, the remote login service starts a user's shell, and that shell parses the command string before the target program reads its arguments. Environment assignments and quoting can change at any layer. Prefer an argument based remote execution API when one exists. When only a shell string exists, test the exact serialized command and include hostile fixtures such as spaces, a single quote, a newline, and text outside ASCII. A locale pin cannot repair a quoting bug, but a quoting bug can make the locale assignment apply to the wrong command.

Treat frequently parsed command output as a small versioned protocol. Store a fixture of the raw bytes, document the expected locale and tool family, and reject shapes the parser does not know. When a tool upgrade changes the shape, update the parser and fixture together. This is less glamorous than making the agent "understand" another display format, and it is far safer for commands that lead to writes.

For SSH, prefer a command that sets its environment remotely over a hope that client forwarding matches. Quote at the correct layer and test values containing spaces, quotes, and text outside ASCII. Avoid adding a login shell merely to inherit a preferred locale, since that also imports aliases, startup scripts, and other state.

Keep raw results available for review. Sallyport can execute SSH actions through its bundled `sp-ssh` helper while keeping SSH keys in its encrypted vault, and its Activity journal records individual calls. That does not make command output locale independent, but it preserves a useful boundary: the agent receives results without receiving the credential used to obtain them.

When an action can change external state, validate the parsed value before the write. Require an explicit format from the read command, reject undecodable bytes, and attach the recorded locale to the proposed action. Human approval is meaningful only when the card shows the value the system actually parsed.

The first repair in an existing suite is concrete. Find every remote command whose output is parsed or snapshotted. Add the environment probe to failures, pin locale and time zone at the remote process, then introduce one comma decimal locale and one different collation locale as adversarial cases. The surprising failures are the assumptions your local shell has been hiding.
