otherbox runs your test command once as it is, then once per environment that differs from yours by exactly one thing — a timezone, a locale, an empty home directory, a temp path with a space in it — and tells you which single change turned it red, with the line to reproduce it.
$ npx otherbox otherbox — one thing different at a time command: npm test baseline pass 6.1s your environment, unchanged tz FAIL 6.3s a clock that is not yours locale pass 6.0s a language that is not yours home pass 7.4s a home directory with nothing in it color pass 6.1s a terminal that wants colour narrow pass 6.0s a narrow terminal ci pass 6.2s the other side of the CI branch clean-env FAIL 6.0s none of your shell spacey-tmp pass 6.1s a temp path with a space in it node skip 0.0s a different Node, if one is parked nearby 2 of 8 environments tested failed. Your suite passes here and would not pass there. 1 environment skipped: node.
A suite is green on the machine that wrote it. That machine has your timezone, your locale, your ~/.gitconfig, your NODE_ENV, your eighty-column terminal and a /tmp with no spaces in it. Every one of those is an assumption your tests are quietly allowed to make, and none of them travels.
The usual way to find out is a red build on someone else's box, or on a runner, or in an issue — a failure with a dozen differences at once and no way to say which one did it. otherbox changes one thing per run, so a failure has exactly one suspect and the fix starts already narrowed.
No plugin, no framework integration, no config file. npm test, pytest, go test ./..., make check — if a shell can run it, otherbox can run it nine more times.
If your suite is already red before anything changes, otherbox stops with exit 2 rather than perturbing a broken baseline and blaming a timezone for it.
npx otherbox # runs npm test in nine environments npm i -D otherbox # or keep it in the project
Requires Node 18.17+. Zero dependencies.
otherbox # default command: npm test otherbox -- npm run test:unit # any command, after -- otherbox --only tz,locale # just these otherbox --skip home,clean-env # all but these otherbox --json # machine-readable, for CI otherbox --repeat 3 # each environment 3 times: flake or finding? otherbox --why clean-env # what a pass there proves — and what it does not otherbox --list # what each environment is and catches otherbox --timeout 120 # seconds per run (default 600)
Exit codes: 0 every environment passed · 1 at least one failed or was flaky · 2 the command was wrong, or your suite already failed — or failed only sometimes — before anything was changed.
Each one changes one thing. Nothing else moves.
| id | one thing different | what it catches |
|---|---|---|
tz | TZ=Pacific/Kiritimati | dates formatted, parsed or compared in the machine's local time. UTC+14 is already tomorrow for most of your working day. |
locale | LC_ALL=tr_TR.UTF-8 | Intl formatting, localeCompare sort order, toLocaleUpperCase — Turkish is the classic (i → İ). |
home | HOME = a fresh empty directory | anything read out of your home that a new machine has not got: ~/.gitconfig, ~/.npmrc, credentials, a warm cache. |
color | FORCE_COLOR=3, no NO_COLOR | output compared as plain text with ANSI escapes switched on — snapshots that match only because the pipe stripped the colour. |
narrow | COLUMNS=40 | output wrapped, padded or truncated to the width of the window that ran it. |
ci | CI flipped to whichever it is not | code and tests that branch on CI — the branch you never take where you can see the output. |
clean-env | everything but a small allowlist removed | what your shell quietly hands the suite: NODE_ENV, npm_config_*, proxies, a token in your profile. |
spacey-tmp | TMPDIR = a path containing a space | paths interpolated into shell commands without quotes; anything assuming a temp path is one word. |
node | PATH prepended with a second Node's bin dir, if one is found nearby | behaviour that only holds on the exact Node version you happen to have installed. Skipped, honestly, if this machine has only one. |
clean-env keeps only PATH, HOME, TMPDIR/TMP/TEMP, SHELL, USER, LOGNAME, PWD, TERM, LANG and the handful of variables Windows needs to start a process.
A red environment prints what it changed, what that change is known to catch, the exact line to reproduce it, and the tail of your own output. No interpretation of your test — just the one variable and your words back.
tz — a clock that is not yours catches: dates formatted, parsed or compared in whatever timezone the machine happens to be in. Pacific/Kiritimati is UTC+14, so for most of your working day it is already tomorrow there. reproduce: TZ=Pacific/Kiritimati npm test last lines: AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: code: 'ERR_ASSERTION', actual: '2026-08-17', expected: '2026-08-18', operator: 'strictEqual'
That is a real run: a date compared against toLocaleDateString, correct in every timezone west of the dateline and wrong in the one otherbox picked.
A single red environment has two explanations: the change broke your suite, or your suite is unreliable and this run is where the coin came up tails. --repeat <n> runs each environment n times and separates them.
$ otherbox --repeat 3 --only tz,clean-env baseline pass 6.1s your environment, unchanged tz FAIL 6.3s a clock that is not yours clean-env flaky 6.0s none of your shell 1 of 2 environments failed every run. Your suite passes here and would not pass there. 1 more was flaky — failed some runs, passed others — which says nothing about the change.
A finding, and it says so: failed all 3 runs — consistent, not a flake.
Reported as flaky and kept out of failed in --json — it gets its own flaky array. The environment is not the suspect; your suite is.
And if the baseline itself fails some runs and passes others, otherbox stops with exit 2 before any environment runs. A command that cannot agree with itself twice cannot have a failure attributed to anything. The exit code still counts a flake as something to look at — the report just refuses to blame a timezone for it.
A green environment is a specific, narrow claim. --why <id> prints the claim and its limits, in the same words as What it can't tell you — so the terminal and the docs cannot drift apart.
$ otherbox --why tz tz — a clock that is not yours changes: TZ=Pacific/Kiritimati A passing tz proves - your command passes with TZ set to a zone fourteen hours ahead of UTC, where the local date is already tomorrow for most of your working day - that nothing under test turns the process timezone into a different date, ordering or format that an assertion then rejects It cannot - prove the code is timezone-correct. One zone is one sample: a negative offset, a half-hour offset (Asia/Kolkata is +05:30) and a DST boundary are all unexercised — Kiritimati has no DST - see a timezone that comes from anywhere but TZ — a config file, a database session, a hard-coded "America/New_York", a browser - move the clock. Only the zone changed; the instant is still now, so nothing here tests a date in the past or the future
--why reads no files and starts no processes — no project, no command, no temp directory. You can read what a run would be worth before you have anything to run.
A test fails if any environment lacks a non-empty It cannot list. A tenth environment cannot ship without someone writing down what it fails to prove.
otherbox --why on its own lists the nine environments, --why all prints every one in full, and --why --json is the same content as data. An unknown id is refused by naming the real ones.
- run: npx otherbox --json > otherbox.json
continue-on-error: true # while you work through what it finds
Or gate on a subset you have already cleaned, and let the rest stay advisory until it is true:
- run: npx otherbox --only tz,clean-env
The JSON report names every environment, its exit code, its duration, the reproduce line and the tail of its output — enough to post a comment or fail a job on a specific id.
The same list is in the README, and it is deliberately the first thing you read there too.
--repeat a test that fails at random looks like a failure of whichever environment it landed on. --repeat <n> is the answer to that question, not a way of hunting flakes generally.node is not a version matrix. Not the OS, not filesystem case-sensitivity, not the CPU architecture, not the network. node reaches outside the environment on purpose but stays honest about it: it runs your command under whichever second Node it finds already installed nearby — one sample, not a range, never downloaded or invented — and is skipped, reported by name, on a machine with only one Node. A green otherbox does not mean your suite passes everywhere — it means it does not depend on these nine things, one of which may not have run at all.clean-env failing may mean your suite genuinely needs a secret. Then the finding is "this suite cannot run on a clean checkout", which is worth knowing and worth writing down.--only, or run it nightly rather than per-commit.locale sets the variables, not the OS locale data. Node's Intl follows LC_ALL regardless, which is what most JavaScript locale bugs turn on; a program that shells out to a C library may need that locale installed to show the same behaviour.