random is a cryptographically secure command-line generator built around an
unusual guarantee: given the same seed and arguments, it emits bit-identical
output across supported operating systems and CPU architectures—including for
normal, exponential, Poisson, log-normal, and beta distributions. True-random
mode instead draws fresh entropy from the operating system CSPRNG and is
intentionally not reproducible.
The project ships three matching implementations: the original LuaJIT oracle, a Zig core exposed through a public C ABI and dogfooded by a C CLI, and a pure-core Rust library with a separate Rust CLI. A seeded BLAKE3 keyed XOF powers cross-platform-identical deterministic streams; stdin operations and multiple output encodings make the same small tool useful beyond number generation.
It ships three equivalent command families: random/nrandom/drandom use
LuaJIT, randomz/nrandomz/drandomz use the C frontend over Zig, and
randomr/nrandomr/drandomr use Rust. The invocation name selects normalized
or deterministic mode in every family.
- Distributions: uniform (default), normal (Box-Muller), exponential, Poisson, log-normal, beta
- Cryptographically secure sources: the platform OS CSPRNG, or a deterministic BLAKE3 keyed XOF (
-d/--seed) - Stdin ops:
--chooseone item,--shuffleall items,--weighted(value:weight) - Output formats: decimal,
--hex,--base64, raw--binaryoutput - Visual distribution help: append
--helpto an alternate-distribution flag for its shape; Kitty and Ghostty receive an embedded PNG via Kitty graphics, WezTerm receives Sixel, and other terminals (including every automatic invocation inside tmux) receive Braille; use--viewto render the shape from parameters supplied on that invocation - Portable continuation: deterministic invocations emit resumable JSON state on stderr; any implementation can continue state from either of the others, without writing state to disk
- Reproducible across platforms: seeded streams are bit-identical across machines, operating systems and CPU architectures — verified on x86_64-glibc, x86_64-musl, aarch64-Linux and native aarch64-macOS — because all math runs on an integer-only kernel instead of the platform's libm — see Determinism below
- Embeddable core:
librandomz.aplusrandomz.h; callers own DRBG state and provide entropy through a callback, so the Zig core performs no I/O - Native Rust library: the
randomrcrate exposes the caller-owned DRBG, integer-only fixed arithmetic, samplers, and curve generation; only its explicitly selectedentropymodule performs I/O - WASM build:
randomz-wasi.wasmexports the same deterministic core plus a fail-closed adapter to the host's WASIrandom_get; the pure Rust core is compile-gated forwasm32-wasip1, while an equivalent Rust artifact is deferred until a measured size/performance comparison decides whether shipping one or both is useful - Small runtime surface: LuaJIT for the oracle CLI; libc for
randomz;randomris a native Rust executable
Seeded streams are bit-identical across runs, machines, operating systems and
CPU architectures. All math runs on an integer-only software float
(lib/fixed.lua) rather than the platform's libm, because libm transcendentals
are not portable: IEEE-754 pins down + - * / sqrt and says nothing about
log, cos or exp. Measured glibc vs musl over this program's actual input
domain, log differs on 0.006% of inputs, cos on 3.06%, and exp on 8.85% —
which meant roughly 3% of seeded "normal" values differed between two builds of
the same source at the same seed.
The deterministic generator accepts only an unsigned decimal integer or a
0x-prefixed hexadecimal integer smaller than 2^256. Both spellings are
serialized as the same 32-byte big-endian value, passed through BLAKE3's
derive-key mode with context random drbg 2026-08-04 v1, then used as the key
for a BLAKE3 XOF over the empty message. Multibyte draws are assembled
big-endian. Default binary output over [0,255] is the contiguous XOF byte
stream; other integer ranges use rejection sampling.
The KDF provides domain separation, not extra entropy. A small or public seed
is reproducible and therefore predictable. If deterministic mode is requested
without a seed, the program obtains 32 bytes from the OS and includes the full
0x-prefixed seed in its JSON stderr state. Compromise of
that seed/key reveals the stream; this interface does not claim backtracking
resistance or state-compromise recovery.
If unpredictability matters, treat the seed as sensitive: command-line arguments, shell history, environment variables, and captured stderr may expose it. The CLI's replay feature is not a secret-storage mechanism.
That portability claim is measured, not argued. On an x86_64-Linux Nix host,
./crossarch (tests/cross_arch_diff) runs three local/emulated legs built
from pinned sources — x86_64-glibc, x86_64-musl, and aarch64-Linux — and
requires byte-identical output from all of them. Set
CROSS_ARCH_REMOTE=peters-macbook-pro-m4-max to add the recorded native
aarch64-macOS Apple-silicon leg (requires ssh and rsync). It also runs two deliberately
fragile controls that must diverge (platform libm across libcs, out-of-range
double→int conversion across architectures), because an "identical" verdict
from a comparison that could not have detected a difference proves nothing.
Results and caveats: docs/cross-architecture-evidence.md.
The same local run compares 54 end-to-end invocations from native x86_64 and
emulated aarch64 Rust executables to the LuaJIT oracle.
While building this kernel we found and filed
LuaJIT/LuaJIT#1499, an
unsigned negate-then-branch trace-compiler miscompilation that silently
corrupted this exact kernel shape. It is fixed upstream, but lib/fixed.lua
still ships a jit.off mitigation, gated at load time on the running
jit.version against the commit that carries the fix — most systems will be
on a pre-fix LuaJIT build for a long time yet. That mitigation is not free:
measured ~5.7x slower on the soft-float distributions (--normalized,
--exponential, --poisson, --log-normal, --beta) while it's active, and
nothing on the integer-only paths. Once your LuaJIT is built from the fix
commit or later, the mitigation switches itself off automatically.
The BLAKE3 migration deliberately breaks all legacy seeded streams and
removes legacy state-file compatibility. Earlier integer-kernel conversion also
changed seeded alternate-distribution output. Fractional range bounds are not
accepted — random 1.5..6.5 is an error, not a silently-truncated range.
The default output delimiter (and default stdin item delimiter for
--choose/--shuffle/--weighted) is always a newline, regardless of
the shell's $IFS. Earlier versions derived the default from $IFS,
which meant a seeded stream's exact bytes — and even which stdin item a
--choose/--shuffle/--weighted call selected — silently depended on
an ambient shell variable neither the seed nor the command line
mentioned. Same seed, same arguments, different $IFS, different
output. --delimiter remains the explicit, documented control for
anyone who wants something other than newline.
random # uniform 0-99
random 1..6 # inclusive uniform range
random 1...7 # same values; three dots exclude 7
random -n --mean 50 --stddev 10 # normal distribution
random --exponential --rate 4 # exponential with rate 4
random --poisson --lambda 5 # --mean 5 remains an exact alias
random --beta=3 --alpha=1 --view # chart Beta(alpha=1, beta=3)
random --exponential --precision 6 # truncate fractional output to 6 places
random -d --seed 42 -c 5 # 5 reproducible numbers
state=$(random -d --seed 42 d20 2>&1 >/dev/null)
random --resume "$state" # continue at the next die roll
printf '%s\n' "$state" | random --state - --count 10
random --true-random -b -c 32 # force entropy even if DRANDOM_SEED is set
random --hex -c 5 # 5 hex values
printf 'a\nb\nc\n' | random --choose
printf 'rare:1,common:10' | random --weighted --delimiter ','Run random -h for the full option list.
Generated values stay on stdout. A deterministic invocation writes one JSON state object to stderr after stdout has been flushed:
{"sv":1,"rv":"0.2.0","seed":"0x000000000000000000000000000000000000000000000000000000000000002a","next_pos":"12","args":{"distribution":"uniform","range":"1..20","count":"3","encoding":"text","delimiter":"\n"},"notices":[],"warnings":[]}next_pos is a decimal string containing the next BLAKE3 XOF byte position,
not an output-item counter. That distinction preserves exact continuation when
rejection sampling or a nonlinear distribution consumes a variable number of
bytes. Seeking to the known position is O(1); deriving the Nth distributed
value from only a seed generally requires replaying the preceding values.
The state deliberately contains no copy of stdout's value or values.
Numbers in args are strings so a JSON parser cannot mutate exact decimals
through IEEE-754 conversion. sv is the enforced state/stream compatibility
version and must change when serialized semantics change; rv is the
informational producer application version and is not a compatibility gate.
Notices and warnings are arrays, and errors use an
error object in the same stderr JSON envelope. Successful true-random calls
with no notice or warning leave stderr empty. Input state must be valid UTF-8;
stdin state is capped at 1 MiB, and structural depth/member limits reject
pathological JSON before it can exhaust a parser stack or monopolize the CLI.
For shell loops that only need to advance state, the redirection order below captures stderr while discarding stdout:
state=$(random -d --seed 42 d20 2>&1 >/dev/null)
state=$(random --resume "$state" 2>&1 >/dev/null)To resume a stdin population operation, pass the state inline so stdin remains available for the population:
printf 'red\ngreen\nblue\n' | random --choose --state "$state"Fractional distributions print 18 deterministic decimal places by default.
Use --precision N or its --truncate N alias to truncate (never round) to
0–18 places; integer and binary stream semantics are unchanged.
Distribution-qualified help is order-independent: for example,
random --normalized --help and random --help --normalized show the normal
curve. The CLI sends its embedded PNG directly through the Kitty graphics
protocol without a temporary file in Kitty and Ghostty. WezTerm uses a
code-generated Sixel rendering of the same plot. Automatic mode always uses
the scrollback-stable UTF-8 Braille fallback inside tmux: terminal image
placements can disappear when scrolling even when tmux and the outer terminal
both advertise a supported protocol. Set
RANDOMZ_CHART_TYPE=utf8|kitty|sixel to override automatic selection.
--utf8 (also --utf8-graphics), --kitty, and --sixel override the environment for one
distribution-help or --view invocation; if repeated, the last renderer flag
wins.
--view requires exactly one alternate distribution, generates no random
bytes, and prints only that distribution's parameter summary and chart. Normal
and log-normal accept --mean and --stddev; either normal parameter can be
supplied independently and the omitted one defaults to mean 0 or standard
deviation 1. Exponential accepts --rate, Poisson accepts --lambda (with
--mean retained as an exact alias), and bare --beta uses beta parameter 2
while --beta B or --beta=B replaces it; --alpha controls alpha. Parameter
options accept both --name value and --name=value. Distribution-qualified
--help deliberately keeps showing the frozen default chart even when
parameter tokens are also present.
Uniform and range-scaled normal modes accept at most one atomic integer range:
M-N and M..N include both endpoints, while Ruby-style M...N excludes N.
For die rolling, dN is an exact alias for 1..N, so random d6,
randomz d20, and randomr d100 select uniformly from the corresponding die
faces. N must be a positive whole number no larger than 2^53. The default
remains 0..99. The former one/two bare endpoint grammar is not accepted.
Ranges are rejected where a distribution's own parameters determine its
output, including custom-parameter normal mode.
--test runs the shared Bash contract suite. Nix installations close over its
tool dependencies; manual Zig and Windows installations require Bash plus the
common Unix command-line tools used by the suite.
True-random mode uses getrandom on Linux, Solaris, and illumos;
arc4random_buf on Apple and the BSDs; BCryptGenRandom on Windows; and
getentropy on other Unix platforms that provide it. An exact-read
/dev/urandom fallback is used only when the selected Unix kernel API reports
that it is unavailable. Any other source error, policy denial, or short read
fails closed. --random-source PATH selects an explicit byte source, chiefly
for deterministic testing; --no-wait requests GRND_NONBLOCK on a
getrandom backend.
The Apple, FreeBSD, OpenBSD, and NetBSD C frontends are cross-compile gated on x86_64 and aarch64. Solaris/illumos and DragonFly have pinned source selectors, but Zig 0.16 currently lacks usable cross-libc support for their full artifacts; native runtime validation remains pending and support is not yet claimed.
For Rust, the cross-build gate covers Linux aarch64, Windows x86_64/ARM64, FreeBSD x86_64, and NetBSD x86_64 in addition to native Linux. Rust 1.97 does not distribute standard libraries for OpenBSD or BSD ARM64, so those are explicit target-library gaps rather than silent skips. Native ARM64 macOS and Windows workflow legs have passed with architecture-sensitive controls, a frozen raw stream plus all nonlinear distributions and stdin shuffling, OS entropy, and the installed CLI self-test.
Backend flags are normalized to numeric 0/1 values and tested by value,
never by macro presence. The build rejects zero or multiple selected backends;
the test suite additionally defines disabled backends as 0 and proves they
remain false. Object-symbol checks pin each target to its intended OS API, and
a fault-injection test proves an OS policy denial cannot become a weak or
deterministic fallback.
The CLIs never persist deterministic state. Reusing a seed restarts the same
stream; omitting it emits a replayable JSON state. --state and its --resume
alias accept that object inline, from -, or from stdin when their value is
omitted. Explicit CLI arguments override inherited args; --state and
--seed are mutually exclusive. Because --choose, --shuffle, and
--weighted use stdin for their populations, those operations require state
inline. The LuaJIT CLI uses
DRANDOM_SEED; the C/FFI CLI uses DRANDOMZ_SEED; the Rust CLI uses
DRANDOMR_SEED. Each frontend ignores the other two namespaces.
An inherited frontend-specific seed variable makes a plain invocation
deterministic, so security-sensitive callers should use --true-random, which
overrides that frontend's environment variable and rejects deterministic flags.
The vendored LuaJIT BLAKE3 implementation is by Egor Skriptunoff. Its header
preserves the author's MIT notice from the verified earlier pure_lua_SHA
ancestor and records the chronology, source commits, and the fact that the
LuaJIT-only gist has no separately visible license.
nix run github:pmarreck/random # run without installing
nix run github:pmarreck/random#randomz # C CLI over the Zig FFI
nix run github:pmarreck/random#randomr # Rust CLI
nix profile install github:pmarreck/randomFor the LuaJIT oracle, put bin/ on your PATH; it requires luajit. For the
C CLI and static library, run zig build -Doptimize=ReleaseFast and use
zig-out/bin/randomz, zig-out/include/randomz.h, and
zig-out/lib/librandomz.a. nrandomz and drandomz are installed aliases;
the Nix package installs them as symlinks.
For Rust, run cargo build --locked --release -p randomr-cli; Cargo emits
randomr, and the Nix package supplies nrandomr and drandomr aliases. Rust
programs can depend on the workspace crate at rust/randomr and use Drbg as a
ByteSource for any exported sampler without involving CLI I/O or formatting.
The same build emits zig-out/bin/randomz-wasi.wasm, a WASI Preview 1 reactor
module. It exports memory, the public deterministic randomz_* ABI, and
randomz_wasi_fill(ptr, len), which returns RANDOMZ_OK on success and
RANDOMZ_ENTROPY_ERROR if the host's random_get fails. A browser can host
the module through a WASI shim whose randomness implementation calls Web
Crypto crypto.getRandomValues; the module never substitutes Math.random
or an internal deterministic fallback. The Nix package installs the module as
lib/randomz-wasi.wasm.
The seed material is exactly 32 bytes; CLI decimal/hex parsing is a frontend convenience. State is caller-owned, must be externally synchronized, and a copy or process fork clones the future stream.
#include <randomz.h>
uint8_t seed[32] = {0};
uint8_t bytes[32];
randomz_drbg rng;
seed[31] = 42;
if (randomz_drbg_init(&rng, seed) != RANDOMZ_OK ||
randomz_drbg_fill(&rng, bytes, sizeof bytes) != RANDOMZ_OK)
return 1;
randomz_drbg_zeroize(&rng);Compile an installed static library with
cc app.c -I$prefix/include $prefix/lib/librandomz.a -o app. Samplers accept a
randomz_fill_fn; returning a known randomz_status preserves that error.
tests/randomz_abi_test.c is the exhaustive consumer
example, including state replay, fixed-format parameters, every sampler, and
error handling. Windows builds produce randomz.lib.
A dev shell with LuaJIT and the test tooling is provided:
direnv allow # or: nix develop
./test # FAST mode (quick, quiet on success)
FAST= ./test # full statistical run
./stats # separate, deeper sanity analysis of all three implementations
nix flake check # hermetic CI check (runs all 19 suites, but FORCES FAST=1 --
# kernel_jit_diff's 60000-iteration deep JIT differential
# is deep-mode-only by design and is SKIPPED here, not run;
# run `FAST= ./test` locally for the full non-FAST suite)./test runs every suite under tests/ (official BLAKE3 vectors, an independent
Zig DRBG reference check, the same 80-check Bash CLI contract against all three
executables, all three pairwise 141-case exact frontend matrices, Rust
mutation/downstream-library controls, a C-compiled public-ABI conformance
test, isolated Zig-package reconstruction, 11-target Zig cross-compilation
(including Windows ARM64), Wine-executed Windows x86_64 parity, kernel unit
tests, golden vectors, the bc sweep, the all-directions continuation
matrix, and the deep-mode-only JIT differential).
Set RANDOM_TEST_CLI to
run tests/random_test or tests/drbg_test against another compatible binary.
The suites are hermetic and concurrency-safe.
Cold ./test and Nix runs are dominated by ReleaseFast compilation and the
multi-target artifact gates, not by the pure arithmetic checks themselves.
./stats is intentionally separate from the correctness suite. It streams raw
bytes and distribution samples without writing them to disk, checks obvious
bias/shape failures, and first proves its thresholds reject deliberately bad
generators. Use ./stats --lua, ./stats --c, ./stats --rust, or
./stats --cli PATH; FAST=1
reduces sample sizes. A pass is a statistical smoke test, not a cryptographic
security certification. The sensitivity set rejects seven deliberately bad
generators: an all-zero byte stream plus constant uniform, normal,
exponential, Poisson, log-normal, and beta samples.
./bm compares release-built LuaJIT, Zig/C, and Rust implementation payloads
over raw, encoded, uniform, and every nonlinear distribution. Before timing,
it streams every seeded workload through SHA-256 and requires matching digests
across all three packaged CLIs and the unwrapped Rust payload; generated bytes
are never written to disk. Rust timing bypasses only the Nix
Bash wrapper that makes installed --test self-contained, so wrapper startup
is not misreported as core computation. Hyperfine runs with --shell=none, so
no shell participates in a measured invocation. The full suite emits 8 MiB or
20,000 samples per process, amortizing the remaining loader and CLI startup;
this is deliberately a direct-process throughput benchmark rather than a
single-draw latency benchmark. Measurements, timing-surface identity, exact
executable hashes, and the tool versions visible to the harness append to
benchmarks/<machine-id>.ndjson. Prior results on the same machine, argument
set, and timing surface provide a two-sided ±15% review threshold: regressions
are loud, and surprising speedups are flagged in case work disappeared. Use
--quick for a short run or --check for only the cross-implementation proof.
The Rust compile-time coefficient change was measured before and after on an AMD Ryzen Threadripper 3990X, using the quick suite's 5,000-sample batches and CPU time (three measured runs after one warmup):
| Distribution | Rust before | Rust after | Improvement | Rust after vs. Zig |
|---|---|---|---|---|
| Normal | 30.92 ms | 19.00 ms | 38.6% | 1.14x |
| Exponential | 21.76 ms | 14.72 ms | 32.3% | 1.30x |
| Poisson | 77.51 ms | 39.16 ms | 49.5% | 0.96x |
| Log-normal | 43.67 ms | 26.09 ms | 40.3% | 1.27x |
| Beta | 77.70 ms | 52.05 ms | 33.0% | 1.04x |
Raw, encoded, and uniform control workloads changed by -2.7% to +4.4%, which supports attributing the nonlinear gains to removal of runtime coefficient division rather than unrelated machine noise or missing work. Every measured payload remained byte-identical across LuaJIT, Zig/C, the packaged Rust CLI, and the unwrapped Rust executable.
bin/random the program (LuaJIT)
bin/nrandom -> random (normalized mode)
bin/drandom -> random (deterministic mode)
lib/fixed.lua integer-only soft-float kernel (see Determinism above)
lib/distribution_charts.lua generated embedded PNG/Sixel/Braille help charts
lib/distribution_view.lua runtime parameter-aware curve/raster oracle
src/fixed.zig independent Zig port of the fixed-point kernel
src/randomz.zig pure RNG/distribution core and exported C ABI
src/randomz_cli.c C CLI; accesses the Zig core only through randomz.h
src/distribution_view.c C-side UTF-8/Kitty/Sixel runtime rasterizer
tools/generate_distribution_charts.lua deterministic shared chart generator
include/randomz.h public caller-owned-state C API
rust/randomr/ importable pure-core Rust library plus isolated entropy module
rust/randomr-cli/ Rust I/O, argument parsing, formatting, and chart frontend
tests/random_test CLI behavior + statistical distribution suite (bash)
tests/randomz_test shared later-frontend contract + exact LuaJIT differential matrix
tests/randomr_test Rust architecture, shared-contract, and two-oracle gate
tests/randomr_mutation_test proves five Rust acceptance controls can turn red
tests/randomr_bsd_targets Rust 1.97 distributed BSD-target cross-link gate
tests/randomz_cross_compile_test Linux/macOS/Windows x86_64/aarch64 build gate
stats standalone raw/distribution statistical sanity analysis
bm three-implementation release benchmark and parity preflight
benchmarks/ per-machine append-only NDJSON benchmark history
tests/fixed_test unit tests for lib/fixed.lua (bash)
tests/golden_test verifies committed golden vectors still reproduce
tests/kernel_bc_sweep sweeps the kernel against `bc -l` as an independent oracle
tests/kernel_jit_diff JIT-vs-interpreter differential control (deep mode only)
crossarch runs the cross-architecture differential (not part of ./test)
tests/cross_arch_diff cross-arch/libc differential + its sensitivity controls
tests/cross_arch_decimal.lua payload: parse/tostring/int paths across platforms
tests/cross_arch_controls.lua positive controls that MUST diverge
tests/bless-goldens run deliberately to regenerate golden vectors; never from ./test
tests/golden/ committed golden vectors (integer.txt, dist.txt)
alternates/ earlier reference implementations (nrandombash, nrandomlua)
flake.nix dev shell, package, and CI check
test test runner
The alternates/ directory keeps earlier single-distribution implementations
(nrandombash, an awk/bash normal generator; nrandomlua, a plain-Lua one) for
lineage and benchmarking. They are not installed.
MIT © Peter Marreck