One CLI-tool architecture, expressed in seven languages. Each *-script
skill teaches the same design β pure logic split from side effects, side
effects hidden behind injectable boundaries, a real test suite, and a
type/test/format gate β adapted to one language's idiomatic toolchain.
Pick the language by need (see Choosing); the shape of the tool is identical everywhere, so moving between them is muscle memory.
scripteratops is built on the Agent Skills open standard (SKILL.md), which
Claude Code, OpenAI Codex, and Gemini CLI all read natively β the skills
auto-trigger by relevance on each. Install with whichever tool you use.
These skill / plugin / extension systems are new and moving fast (late 2025 / early 2026). If a command below has drifted, check your tool's current docs β the Any tool fallback always works.
In an interactive claude session:
/plugin marketplace add https://github.com/buildosaurus/scripteratops
/plugin install scripteratops@scripteratops
/plugin marketplace update scripteratops # later, to pull new versions
codex plugin marketplace add buildosaurus/scripteratops
Then install scripteratops from the plugin directory. Codex reads the same skills and implicit invocation is on by default, so they auto-trigger as in Claude.
gemini extensions install https://github.com/buildosaurus/scripteratops
Gemini asks for a one-time consent the first time each skill activates.
Codex and Gemini both scan ~/.agents/skills/, so you can skip the manifests
entirely:
git clone https://github.com/buildosaurus/scripteratops
cp -R scripteratops/skills/* ~/.agents/skills/
Prefer just one skill, by hand? For Claude Code, copy a single folder into your config instead:
git clone https://github.com/buildosaurus/scripteratops
cp -R scripteratops/skills/rust-script ~/.claude/skills/rust-script
Installing everything is cheap β a skill is free when idle (it only triggers when relevant), so there's no cost to having all eight.
Skills trigger from what you ask β you don't invoke them by name. Just describe the tool:
- Name the language and its skill runs directly:
"Write a Rust CLI that fetches a URL and prints the JSON pretty-printed."
- Skip the language and the router picks one from your constraints, then hands
off:
"I need a small command-line tool that shells out to
git, ships as a single static binary, and has tests."
Either way you get the same shape β pure logic, injected boundaries, an exit-code contract, and a passing type/test/format gate.
Not sure which language? Start with the router β scripteratops
picks one from your constraints and hands off. The seven language skills:
- Boundaries are the only mock hooks. Everything that leaves the process β subprocess, network, filesystem, environment β sits behind one seam (an interface/trait/protocol, or a wrapper function in shell). Tests inject a fake; production wires the real one.
- Pure logic is separate β input β output, no I/O, tested by direct call.
- Config precedence, resolved in one pure function: `CLI flags > environment
config file > defaults`. Secrets get no CLI flag (argv is world-readable); a config is never logged raw (secret fields are redacted).
- Colorized logging to stderr (stdout is for program data),
-v/-qverbosity,--color auto|always|never. --dry-runshort-circuits before mutating anything.- Typed errors mapped to an exit-code contract:
0ok Β·1runtime Β·2usage (where the CLI library cooperates β Python/Rust/shell emit2; Clikt and Cliffy map usage errors to1, see caveats) Β·124subprocess timeout Β·130SIGINT Β·143SIGTERM (141broken pipe where the language surfaces it). - Self-contained & pinned β one file or one project, dependencies pinned, a
man page via the toolchain's generator or
help2man.
| Language | Packaging / run | CLI library | Tests | Type gate | Format + lint | Distribution |
|---|---|---|---|---|---|---|
| Python | uv + PEP 723 (single file) | argparse (stdlib) | pytest | mypy | ruff | uv run (+ install.py) |
| Swift | SwiftPM package | swift-argument-parser | Swift Testing | compiler | swift-format | swift build -c release |
| Kotlin | Gradle (core+app) |
Clikt | JUnit 5 + kotlin.test | compiler | ktlint | installDist / GraalVM native |
| TypeScript | Deno (one project) | Cliffy | deno test |
deno check (tsc) |
deno fmt + deno lint |
deno compile (single binary) |
| Go | Go module | cobra | go test |
compiler | gofmt + go vet |
go build (single binary) |
| Rust | Cargo (lib+bin) |
clap (derive) | cargo test |
compiler | rustfmt + clippy | cargo build --release (single binary) |
| Shell | one Bash script | hand-rolled while/case |
bats-core | β (shellcheck) | shfmt + shellcheck | the script itself (chmod +x) |
The type gate is free (the compiler) everywhere a language has one; Python adds mypy as a separate step, and shell has no type system so shellcheck plays that role.
| Language | Config source | HTTP client | Subprocess | Indent |
|---|---|---|---|---|
| Python | TOML / JSON (stdlib) | httpx | subprocess |
4-space |
| Swift | JSON (Codable) |
URLSession | Foundation.Process |
2-space |
| Kotlin | JSON (kotlinx.serialization) | java.net.http |
ProcessBuilder |
2-space |
| TypeScript | JSON | fetch |
Deno.Command |
2-space |
| Go | JSON (encoding/json) |
net/http |
os/exec |
tabs |
| Rust | JSON (serde_json) | ureq | std::process |
4-space |
| Shell | key=value file |
curl | direct (timeout) |
2-space |
You only need the toolchain for the language(s) you actually use β each is a one-line install.
| Toolchain | macOS | Linux | Windows (native) |
|---|---|---|---|
| uv (Python) | brew install uv |
curl -LsSf https://astral.sh/uv/install.sh | sh |
winget install astral-sh.uv |
| Deno (TypeScript) | brew install deno |
curl -fsSL https://deno.land/install.sh | sh |
winget install denoland.deno |
| Go | brew install go |
distro package / go.dev/dl | winget install GoLang.Go |
| Rust | brew install rust (or rustup) |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
winget install Rustlang.Rustup |
| JDK (Kotlin) | brew install openjdk (or SDKMAN!) |
SDKMAN! / distro package | winget install EclipseAdoptium.Temurin.21.JDK |
| Swift | Xcode CLT (xcode-select --install) |
swiftly / swift.org | swift.org installer (rougher) |
| Shell tools | brew install bash shellcheck shfmt bats-core |
distro package (apt install shellcheck β¦) |
via Git Bash / WSL |
- uv is recommended, not required β a dependency-free Python script runs with
plain
python3; uv (orpipx run) only becomes necessary once the script pins third-party deps, where it builds the ephemeral env for you. uv can even install Python itself (uv python install). - Kotlin needs only a JDK β the Gradle wrapper is committed, so there's no separate Gradle install.
- Man pages install into a MANPATH directory on macOS/Linux; there is no
native
manon Windows.
The five cross-platform toolchains β uv, Deno, Go, Rust, and the JDK (Kotlin) β
run natively on modern Windows (winget covers them all). The two Unix-centric
variants are the exceptions:
- Shell isn't native β run it under Git Bash (ships with Git for Windows) or WSL.
- Swift has a native Windows toolchain, but it's less mature than on macOS / Linux / WSL.
manpages don't exist on native Windows.
For a uniform experience β and to get the Unix-centric tools cleanly β WSL (Windows Subsystem for Linux) gives you a full Linux environment where all seven work exactly as on Linux. And whatever you develop on, Go / Rust / Deno binaries cross-compile to Windows regardless.
Every variant carries a wart or two β better named than discovered:
- Python β the only one where the type check is a separate gate (
mypy). uv is recommended, not required: a dependency-free script runs under plainpython3 script.py(the PEP 723 header is just a comment), andpipx runis an alternative β uv only earns its keep once the script pins third-party deps. Mind theuv runflag order (uv run --with X tool β¦, flags fortoolgo after its name). - Swift β
swift test(Swift Testing) needs the Xcode toolchain (DEVELOPER_DIR), not the bare Command Line Tools; the binary is per-OS and the ecosystem is Apple-centric. - Kotlin β runs on the JVM (needs a JDK to build, a JRE to run the app image); the first Gradle build is slow; the Gradle wrapper is committed for reproducibility; Clikt maps usage errors to exit 1, not 2.
- TypeScript β Deno is deny-by-default: the tool declares
--allow-run/net/env/read(baked intodeno compile); Cliffy's usage errors exit non-zero but not the conventional 2. - Go β the module name must be path-form (
example.com/tool), not a bare word (a bare name tripsgo mod tidy); tabs (gofmt is non-configurable); cobra groups subcommands, not flags, so--helplists all flags flat. - Rust β no HTTP or JSON in std, so serde + ureq earn their place; the
subprocess timeout is hand-rolled (std has none); Rust ignores SIGPIPE,
so a
tool | headcan panic on a broken pipe (handle the write error if you stream); 4-space (rustfmt); the only variant that natively uses exit 2 for usage errors. - Shell β no type system (shellcheck is the safety net); the CLI is
hand-rolled (
-vvbundling not parsed β repeat-v -v); a hard subprocess timeout needs GNUtimeout(macOS βbrew install coreutils); no JSON withoutjq; needs bash 4+ (macOS ships 3.2 βbrew install bash).
| If you need⦠| Reach for |
|---|---|
| A single self-contained binary, cross-platform, systems-level speed | Go (simplest, fast builds, glue-first) or Rust (strictest, safest) |
| Ubiquitous glue with zero toolchain, present on every box | Shell (bash 4+) β or Python for anything non-trivial |
| Rapid prototyping, data wrangling, the largest library set | Python |
| To live in the JS/TS ecosystem, or a modern batteries-included runtime | TypeScript (Deno) |
| Apple platforms, or an existing Swift codebase | Swift |
| The JVM / Android, or an existing Kotlin codebase | Kotlin |
Rules of thumb:
- Distribution matters most? Go and Rust ship one self-contained binary with no separate runtime (statically linked to varying degrees β fully static on Linux via a musl target); Deno compiles one too; a shell script needs only bash; Python needs uv; Swift/Kotlin are platform- or JVM-bound.
- Correctness matters most? Rust's type system is the strictest, then Swift/Kotlin/Go/TypeScript (all statically typed), then Python (opt-in via mypy), then shell (shellcheck, no types).
- Speed of writing matters most? Python and shell are the quickest to a first working version; the compiled languages trade a little ceremony for a binary and stronger guarantees.
Every *-script skill ships a SKILL.md (the decisions) plus an assets/
template that is a real, gate-passing project β copy it, adapt it, delete what
the tool doesn't earn. (The scripteratops router is the exception: it carries
only a SKILL.md, since its job is to choose a language and hand off, not to ship
a template.) A request for a tool in language X produces the project shape, the
CLI contract, the exit codes, and only the robustness rules the job triggers
(shelling out earns the timeout + preflight; the network earns the HTTP boundary +
timeout; holding config/secrets earns precedence + redaction).
MIT β see LICENSE.