Shell out without the footguns
A type-safe, injection-proof shell runner with typed output parsers — three native implementations.
Open source · MIT license · Rust · TypeScript · Kotlin
import { sh } from "@slothlabs/shx";
const branch = await sh`git rev-parse --abbrev-ref HEAD`.text();
const pods = await sh`kubectl get pods -o json`.json<PodList>();
// Safe even if `id` is "foo; rm -rf / #":
await sh`docker stop ${id}`;
// argv === ["docker", "stop", "foo; rm -rf / #"] ← one argument, no shellshx runs shell commands without a shell: every interpolated value is exactly one argument, so there is nothing to break out of. Parsing (text, json, lines, csv) is a first-class part of running. Native in Rust, TypeScript, and Kotlin.
Shelling out is either unsafe (string concatenation — hello injection) or stringly-typed (you hand-parse every stdout). shx fixes both. Commands are built from an argument list and spawned without a shell, so every interpolated or user-supplied value is exactly one argument. There is no string a caller can pass that breaks out into another command, because there is no shell to break out of.
Output is typed, too: text, lines, json, csv, and custom parsers are part of running a command, not an afterthought. A rich error carries exitCode, stdout, stderr, timedOut, and durationMs, so you never re-run just to see what went wrong. The dependency footprint stays tiny in every language — TS uses tagged templates, Rust a builder plus a cmd! macro, Kotlin varargs — but the one-value-equals-one-argument safety boundary is identical everywhere.
Built to get out of your way
Injection-proof by construction
Commands spawn directly with an argv array — no shell. sh`docker stop ${id}` is safe even if id is "foo; rm -rf / #": it is passed as one literal argument. There is nothing to escape because there is no shell to escape from.
Typed output, first-class
text(), lines(), json<T>(), csv, and parse(fn) make parsing part of running. Get a typed PodList from kubectl get pods -o json, or rows from a CSV with the same call that ran the command.
Errors you can read
A non-zero exit, spawn failure, or timeout throws a rich error carrying exitCode, signal, stdout, stderr, timedOut, and durationMs. Opt out with nothrow/allowExitCodes when a non-zero exit is expected (hello, grep).
Add it in one line
Same library, three ecosystems. Pick yours.
cargo add shx --git https://github.com/slothlabsorg/shx --features serdenpm i @slothlabs/shxrepositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.slothlabsorg:shx:v0.1.0")
}npm packages publish under the @slothlabs scope · JitPack builds from the git tag v0.1.0
One small example
A type-safe, injection-proof shell runner with typed output parsers — three native implementations.
import { sh } from "@slothlabs/shx";
const branch = await sh`git rev-parse --abbrev-ref HEAD`.text();
const pods = await sh`kubectl get pods -o json`.json<PodList>();
// Safe even if `id` is "foo; rm -rf / #":
await sh`docker stop ${id}`;
// argv === ["docker", "stop", "foo; rm -rf / #"] ← one argument, no shellTry shx in your stack
Free and open source. Rust, TypeScript, or Kotlin — same semantics everywhere.