runner is for people who bounce between codebases and refuse to memorize each repo’s private little task-running religion.
Instead of guessing whether this one wants npm run, pnpm exec, bun x,
cargo, uv run, deno task, turbo, make, just, etc. type:
run <TAB>run ran in this very project
❯ run
run 0.12.2
Package Managers bun, cargo
Task Runners just
Node 24.14.1
Monorepo yes
just build-packages
just default
just gen-schema Drift guard: just gen-schema && git diff --exit-code schemas/
just install
just ls
just run
just runner
just test-release Build release bin and verify the facade shims spawn the native binary.
cargo b build
cargo bb build --bin run --bin runner
cargo bbr build --bin run --bin runner --release
cargo bin-run run --quiet --bin run
cargo bin-runner run --quiet --bin runner
cargo c check
cargo cl clippy --all-targets --all-features
cargo comp run --quiet --bin runner -- completions
cargo d doc
cargo f run --quiet --bin run -- --pm npm dprint fmt
cargo format run --quiet --bin run -- --pm npm dprint fmt
cargo i install --path .
cargo l clippy --all-targets --all-features -- -D warnings -D clippy::all
cargo lint clippy --all-targets --all-features -- -D warnings -D clippy::all
cargo man run --quiet --features man -- man
cargo meta metadata --format-version 1
cargo r run
cargo rbin-run run --quiet --bin run --release
cargo rbin-runner run --quiet --bin runner --release
cargo rm remove
cargo rq run --quiet
cargo rr run --release
cargo runner run --quiet --bin runner
cargo schema run --quiet --features schema -- schema
cargo t testand run <TAB> (zsh):
❯ run <TAB>
-- just --
build-packages run
default runner
gen-schema -- Drift guard: just gen-schema && git diff --exit-code schemas/ just:runner
install test-release -- Build release bin and verify the facade shims spawn the native binary.
ls
-- cargo (aliases) --
b -- → build lint -- → clippy --all-targets --all-features -- -D warnings -D clippy::all
bb -- → build --bin run --bin runner man -- → run --quiet --features man -- man
bbr -- → build --bin run --bin runner --release meta -- → metadata --format-version 1
bin-run -- → run --quiet --bin run r -- → run
bin-runner -- → run --quiet --bin runner rbin-run -- → run --quiet --bin run --release
c -- → check rbin-runner -- → run --quiet --bin runner --release
cl -- → clippy --all-targets --all-features rm -- → remove
comp -- → run --quiet --bin runner -- completions rq -- → run --quiet
d -- → doc rr -- → run --release
f -- → run --quiet --bin run -- --pm npm dprint fmt cargo:runner -- → run --quiet --bin runner
format -- → run --quiet --bin run -- --pm npm dprint fmt schema -- → run --quiet --features schema -- schema
i -- → install --path . t -- → test
l -- → clippy --all-targets --all-features -- -D warnings -D clippy::all
-- Options --
--dir -- Use this directory instead of the current one
--pm -- Override the detected package manager (also reads RUNNER_PM when omitted). Valid: npm, yarn, pnpm, bun, cargo, deno, uv, poetry, pipenv, go, bundler (alias: bundle), composer
--runner -- Override the detected task runner (also reads RUNNER_RUNNER when omitted). Valid: turbo, nx, make, just, task (alias: go-task), mise, bacon
--fallback -- What to do when no detection signal matches: probe (default, PATH probe), npm (legacy silent fallback), error (refuse). Also reads RUNNER_FALLBACK when omitted.
--on-mismatch -- What to do when the manifest declaration disagrees with the lockfile: warn (default), error (exit 2), ignore (silent). Also reads RUNNER_ON_MISMATCH when omitted.
--explain -- Print a one-line trace describing how the package manager was resolved. Also enabled when RUNNER_EXPLAIN is set to a truthy value.
--no-warnings -- Suppress all non-fatal warnings on stderr. Also enabled when RUNNER_NO_WARNINGS is set to a truthy value.
-q, --quiet -- Graduated output policy: -q runner progress, -qq warnings + safe host quiet, -qqq recoverable error decoration, -qqqq mute. Task streams survive. RUNNER_QUIET accepts numeric levels; nested runners inherit it.
--host-stream -- Keep the host tool's stdout clean by diverting its diagnostics to stderr: inherit (default) | stderr. Only pnpm can (via --use-stderr); other hosts no-op. Also reads RUNNER_HOST_STREAM.
--schema-version -- Pin JSON output schema version (currently always 1). Affects --json output of doctor/list/why only.
--sequential -- Run the given tasks sequentially. Conflicts with `--parallel`
--parallel -- Run the given tasks in parallel. Conflicts with `--sequential`
--keep-going -- Run every task in the chain regardless of failures. Conflicts with `--kill-on-fail`
--kill-on-fail -- Parallel only: SIGKILL siblings on first failure. Accepted but unused in sequential mode
--help -- Print help
--version -- Print versionrunner detects the project, finds its tasks, and completes them through one command.
Use the same shape everywhere:
run <TAB>
runner install test build deployLet each repo decide what the tasks actually mean.
npm install -g runner-runOr:
cargo binstall runner-runOr on Arch Linux:
paru -S runner-run-bin # or `paru -S runner-run` (builds from source)
yay -S runner-run-bin # .. `yay -S runner-run`Other install methods
cargo install runner-run
cargo install --git=https://github.com/kjanat/runner/ runner-run
cargo install --path .# AUR source build (compiles via cargo):
yay -S runner-run# Container image, for build stages that run `run`-form package scripts:
COPY --from=ghcr.io/kjanat/runner:0.26.1 /run /usr/local/bin/run
COPY --from=ghcr.io/kjanat/runner:0.26.1 /runner /usr/local/bin/runnerAlso on Docker Hub as kjanat/runner. The image is scratch plus
two musl-static binaries, so one tag serves Alpine and Debian stages alike.
It also runs directly, if you'd like:
docker run --rm kjanat/runner:0.26.1 --version
docker run --rm --entrypoint /run -v "$PWD:/w" kjanat/runner buildWithout -v/--volume, the container sees no project.
Only --version or --help are meaningful without a mount.
# One-liner (latest):
curl -fsSL https://raw.githubusercontent.com/kjanat/runner/master/install.sh | sh
# Or download then run, optionally pinning a version:
curl -fsSLO https://raw.githubusercontent.com/kjanat/runner/master/install.sh
sh install.sh
sh install.sh 0.12.2
sh install.sh v0.12.2The installer also works on Termux/Android (aarch64, best-effort) from runner
release v0.24.0, where it selects a native aarch64-linux-android binary.
Earlier releases lack that asset, so the installer refuses them on Android.
Releases after v0.25.1 carry GitHub build-provenance attestations on every
release archive, the man-page tarball, and the container image.
mise install github:kjanat/runner checks them on its own; by hand:
gh attestation verify runner-v<version>-<target>.tar.gz --repo kjanat/runner
gh attestation verify oci://ghcr.io/kjanat/runner:<version> --repo kjanat/runnerUse the action to install runner in CI (view on marketplace):
- uses: kjanat/runner@master
- run: runner install --frozen test buildThe action downloads the platform package from npm, checks it against the
registry integrity hash, and runs gh attestation verify on it when GitHub
holds a build-provenance attestation for that tarball. verify: require makes
a missing attestation fatal; verify: off skips the check.
- uses: kjanat/runner@master
with: { version: "0.26", verify: require }runner install is not a task; it runs the project's toolchain command(s)
(npm ci, cargo fetch, uv sync, …), then chains the listed tasks
(test, then build) sequentially.
That is the point: the workflow stays boring even when the project underneath is npm, pnpm, bun, Cargo, Deno, uv, Make, just, or whatever automation that repo uses.
A chain of more than one task closes with a roll-up on stderr, so a failure in
a long --keep-going run does not have to be found by scrolling:
· summary: 7 tasks, 5 ok, 1 failed, 1 skipped (exit 1, first failure)
· ✓ typecheck 0.9s
· ✗ test:bun 2.1s (exit 1)
· – test:regex skipped
Under Actions, each failed task also lands in the Annotations panel. The
annotations follow [github].group_output; the roll-up itself does not, so
opting out of Actions decoration keeps the summary. --quiet silences both,
along with everything else runner prints.
Quiet is a four-rung output policy:
| Level | Runner output | Host diagnostics |
|---|---|---|
-q |
Hide progress, groups, task timing, summary, and parallel prefixes | unchanged |
-qq |
Also hide non-fatal warnings | safe host quiet mode |
-qqq |
Also hide recoverable error decoration | stronger reduction when safely supported |
-qqqq |
No runner-authored text; preserve exit status | strongest safe reduction |
Larger counts clamp to mute. Task stdout and stderr survive every rung.
Suppressing either requires explicit per-task stdout = "discard" or stderr = "discard". RUNNER_QUIET accepts the same numeric levels and the resolved
level is inherited by nested runner processes. Explicit --explain stays
visible and reports the effective categories, host arguments, and safe fallback.
Host flags are adapter-specific, never inferred from similar names. See the host quiet support matrix for exact flags, stream effects, exclusions, and version caveats.
Orthogonally, --host-stream stderr (RUNNER_HOST_STREAM) asks the host to
keep stdout clean by routing its diagnostics to stderr. Only pnpm has the
primitive (--use-stderr); elsewhere it no-ops. It composes with any quiet
level.
Runner categories and host diagnostics are independently configurable in
[runner] and [host]. An explicit -q merges with them and the quietest
setting wins: -q with [runner] warnings = false hides both progress and
warnings, and no config value re-enables something a quiet rung hides. Per
task, [tasks.<key>] sets the host level, host stream, task streams, and the
progress, groups, and task_timing switches for that task alone.
For example, keep only the final chain summary while suppressing all other runner-authored output:
[runner]
progress = false
warnings = false
errors = false
groups = false
task_timing = false
summary = true
[host]
diagnostics = "reduced"Task process output remains inherited. Set each task's stdout/stderr to
"discard" when those streams should also be silent.
Install mechanics and outputs
The action resolves the platform's @runner-run/* package, downloads its
tarball from the npm registry, verifies the sha512 integrity npm publishes for
it, and extracts the runner / run binaries. Fetching the tarball directly
avoids the node startup and dependency-tree resolution of npm install, and the
sub-second download beats any cross-job cache restore, so the action always
fetches fresh. The install dir is added to PATH and the binary is smoke-tested
with runner --version.
| I/O | name | description |
|---|---|---|
| Input | version |
Version to install; defaults to latest; accepts exact pins and v? prefixes |
| Output | version |
Concrete version reported by the installed runner --version smoke test |
| Output | bin-dir |
Directory holding the runner / run binaries; added to PATH |
| Output | runner-bin |
Full path to the runner binary |
| Output | run-bin |
Full path to the run binary |
Network fetches retry twice on failure or stall. Exact X.Y.Z pins are checked
against the executed CLI version; a mismatch fails the action.
runner # show detected project info
runner <task> [-- <args...>] # run a task
runner run <target> [-- <args...>] # run a task or command
run <target> [-- <args...>] # alias for `runner run`
runner install [--frozen] [--no-scripts|--scripts] # install dependencies
runner clean [-y] [--include-framework]
runner list [--raw] [--json] # list available tasks
runner info [--json] # show detected project info
runner doctor [--json] # show every resolver signal
runner why <task> [--json] # explain how a task would dispatch
runner config <init|show|validate|path> # manage runner.toml
runner completions [<shell>] [-o <path>]schema_version is 1 and bumps when a field is removed, renamed, or
retyped. Ignore keys you do not know. Schemas live in schemas/.
Runner's own flags go before the task; everything after it belongs to the task, including flags that spell the same as runner's:
run -q tsc -p tsconfig.json --noEmit # -q is runner's, -p is tsc'sThe rule holds through nested dispatch, so a package script may delegate to
another task without counting -- delimiters:
{ "scripts": { "typecheck": "run -q tsc -p tsconfig.json --noEmit" } }-- is still accepted for a task whose name starts with a hyphen, and for
readability.
runner completions generates dynamic shell completion registrations.
For bash, zsh, and fish, runner can auto-detect $SHELL:
eval "$(runner completions)"...or get explicit with it
eval "$(runner completions bash)"
eval "$(runner completions zsh)"
eval "$(runner completions fish)"runner completions powershell | Out-String | Invoke-ExpressionThe generated registration includes runner and, when the sibling run binary
exists next to it, run too.
So after setup, this is the workflow:
run <TAB>No per-project command archaeology. No guessing whether this one wants npm, Cargo, Make, just, Deno, uv, or some handcrafted nonsense from 2021.
man runner and man run (plus man runner-<subcommand>) ship with every
install channel, AUR (runner-run / runner-run-bin), npm
(npm i -g runner-run), crates.io, and install.sh. The pages are rendered
from the CLI definition at release time, not committed.
runner run <target> first looks for a matching task.
If no task exists, runner tries <target> as a local file, then as an
installed dependency: run @typescript/native reads
node_modules/@typescript/native/package.json and runs the binary it
declares, without touching the network. A package that declares several
binaries none of which is named after it, or none at all, is reported rather
than guessed at.
Failing that, it executes <target> through the detected toolchain where
appropriate, such as:
npm exec / npx, yarn run / yarn exec, pnpm exec, bun x,
deno x, uvx, go run
For package managers without a matching exec primitive, runner falls back to
executing <target> directly from PATH.
A task that resolves back to itself through a nested runner/run is
refused with the cycle it found (package.json:tsc -> package.json:tsc)
instead of spawning copies of itself.
--pm says who installs and who invokes a script. --runtime says what the
script and the binaries it shells out to actually execute on. Each runtime uses
its own script runner, file runner and package-exec primitive; the package
manager gets no vote:
--runtime |
package.json script |
local file | ad-hoc binary |
|---|---|---|---|
node |
node --run <task> |
node <file> |
npx |
bun |
bun --bun run <task> |
bun <file> |
bun x --bun |
deno |
deno task <task> |
deno run <file> |
deno x |
run --runtime bun build # bun --bun run build
run --runtime bun ./cli.js # bun ./cli.js, even with a #!/usr/bin/env node line
run --runtime bun eslint . # bun x --bun eslint .bun run build starts the script under bun, but a dependency bin carrying a
#!/usr/bin/env node shebang still resolves to system Node. --runtime bun
adds bun's --bun, which puts that bin on bun too. It applies regardless of
which package manager wrote the lockfile, and it outranks a local file's #!
line, which is how it reaches node_modules/.bin entries.
node --run is Node's own script runner (Node 22+). It deliberately skips
pre<task> / post<task> lifecycle scripts, which npm run, bun run and
deno task all execute; runner warns when the task you dispatch has one.
A local file on deno runs with filesystem, network, environment, subprocess
and system-info permissions granted (--allow-read, --allow-write,
--allow-net, --allow-env, --allow-run, --allow-sys), since deno run <file> denies these by default and ignores the file's shebang. Deno's default
remote-import allowlist is left in place, so a file cannot import and execute
code from an arbitrary host, matching how node and bun already refuse remote
imports. This applies to any detected Deno project, not only --runtime deno.
A runtime you set never applies silently to nothing. When the task that wins
selection comes from a source with no runtime to choose (make, just,
Taskfile, turbo, cargo, …), runner says so. package.json (and deno.json
under --runtime deno) outranks the other sources while a runtime is forced,
so --runtime bun build in a turborepo runs the script rather than the turbo
task.
Set it per project with [runtime].js, or per invocation with
RUNNER_RUNTIME. Nested runner/run calls inherit it.
The run binary is equivalent to runner run, so:
run clean
run installruns a project task named clean or install when one exists, even though
those names are also built-in runner subcommands. When no such task exists, a
bare built-in verb (install, clean, list, info, completions) falls
back to that built-in's default form (so run install installs dependencies)
rather than the package-manager exec path.
The explicit subcommand is the inverse: runner install (and runner clean,
runner list, …) is always the built-in and never runs a same-named task;
use run install / runner run install to reach a task called install.
Auto-detection needs no config. To override it per project, drop a
runner.toml at the repo root. Scaffold one with every knob documented:
runner config init # write a commented runner.toml (--force to overwrite)
runner config show # print the effective config (--json for machine output)
runner config validate # parse + check it; exit 2 on error
runner config path # print the resolved runner.toml pathSettings layer, highest priority first: CLI flags → RUNNER_* env vars →
runner.toml → manifest declarations (packageManager, devEngines).
runner config init writes a #:schema directive on line 1, so editors with a
TOML language server (tombi, taplo) get autocompletion and validation with no
extra setup.
#:schema https://kjanat.github.io/runner/schemas/runner.toml.schema.json
# Force the package manager per ecosystem, overriding lockfile detection.
[pm]
node = "pnpm" # npm | pnpm | yarn | bun | deno
python = "uv" # uv | poetry | pipenv
[runner]
progress = true
warnings = true
errors = true
groups = true
task_timing = true
summary = true
fatal_errors = true
[host]
diagnostics = "normal" # normal | quiet | reduced
stream = "inherit" # inherit | stderr
# Per-task configuration, keyed by task name the way Cargo's [dependencies] is
# keyed by crate name. `prefer` (global rank) and `overrides` (legacy per-task
# pin map) are reserved keys; every other key is a task entry. A task entry is
# either a string (shorthand source/runner pin, like serde = "1.0") or a table
# of settings (runner, verbosity, ... like serde = { version, features }).
# Labels are task runners, package managers (bun, npm, ... map to package.json),
# or source names (package.json). Rank-only: unlisted sources still run. An
# explicit qualifier (package.json:test), --runner, or --pm still outranks these.
[tasks]
prefer = ["turbo", "bun"] # global order: turbo, then package.json
overrides = { dev = "bun", build = "turbo" } # legacy per-task pins beat the order
# Task entries (Cargo-[dependencies] style):
# build = "turbo" # string → source/runner pin
# test = { runner = "bun", verbosity = "quiet" } # table of per-task settings
# [tasks.lint] # sub-table form
# verbosity = { level = "quiet", stream = "stderr" } # off|quiet|very-quiet|silent|mute
# stdout = "inherit" # inherit | discard
# stderr = "inherit" # inherit | discard
# progress = true # this task's dispatch arrow
# groups = true # this task's GitHub Actions group
# task_timing = true # this task's chain timing line
# `verbosity` is the per-task form of the -q / --host-stream flags: a string
# (off|quiet|very-quiet|silent|mute) or a { level, stream } table, deep-merged under
# any global flag/env. It quiets the host tool for that task. The runner's own
# lines for the task follow `progress`, `groups`, and `task_timing`; a global
# `-q` or `[runner]` false still wins over a per-task true.
#
# Keys layer from least to most specific, per axis: `site` (every task by that
# name), `package.json:site` (that source), `rfc:site` (workspace member `rfc`),
# `rfc:package.json#site` (the FQN `doctor --json` prints).
# [tasks."rfc:site"]
# stdout = "discard"
# Deprecated, superseded by [tasks] above. Legacy ranked allow-list of task
# runners that also *restricts* candidates (a same-named task under an unlisted
# runner is rejected). Still honored for existing configs, with a warning.
# [task_runner]
# prefer = ["just", "turbo"] # turbo, nx, make, just, task, mise, bacon
# Restrict which detected package managers `runner install` runs. Empty/absent
# installs every detected PM. Overridden by RUNNER_INSTALL_PMS
# (comma-separated). `[pm]` above only scopes script dispatch, not the install
# fan-out.
# `on_collision` decides what happens when two of them write the same directory
# (bun and a nodeModulesDir-enabled deno both writing node_modules). "resolve"
# (the default) installs with the PM the resolver already picked for the
# ecosystem and skips the other, saying so; naming both in `pms` runs both, one
# after another over the shared tree. "error" refuses to pick and exits 2.
# Overridden by RUNNER_INSTALL_ON_COLLISION.
# `scripts` controls install-time lifecycle scripts (the main supply-chain
# attack surface): "deny" skips them where the PM allows it
# (npm/yarn/pnpm/bun/composer; deno already denies); "allow" forces them on
# where the PM can express it (npm --no-ignore-scripts, yarn-berry
# YARN_ENABLE_SCRIPTS=true, deno --allow-scripts), useful now that npm/pnpm are
# moving to scripts-off-by-default. bun and pnpm (>=10) can't be forced on by a
# flag (their dependency build scripts need a trustedDependencies /
# onlyBuiltDependencies manifest allowlist runner won't write), so they warn.
# Precedence: CLI --no-scripts/--scripts > RUNNER_INSTALL_SCRIPTS > [install].scripts.
[install]
pms = ["bun"] # only install with these; each must be detected
scripts = "deny" # deny | allow (absent = each PM's own default)
on_collision = "resolve" # resolve (one writer per install dir) | error
# Resolver policy knobs.
[resolution]
fallback = "probe" # probe (PATH probe) | npm (legacy) | error
on_mismatch = "warn" # warn | error (exit 2) | ignore (manifest vs lockfile)
# Failure policy for `-s`/`-p` chains and `install <tasks>`.
# keep_going and kill_on_fail are mutually exclusive; setting both is an error.
[chain]
keep_going = false # run every task despite failures (same as -k)
kill_on_fail = false # parallel: kill siblings on first failure (same as -K)
# GitHub Actions output grouping (active only under Actions).
[github]
group_output = true # ::group:: each task; annotate failed chain tasks
group_parallel = true # buffer parallel tasks, print each as one block
# Parallel (`-p`) output presentation outside GitHub Actions.
[parallel]
grouped = false # buffer + print each task as one block on completionUnknown keys are rejected at parse time. Every field is optional; omit a
section to keep its defaults. A committed JSON Schema lives at
schemas/runner.toml.schema.json for
editor autocompletion.
runner ships a language server for runner.toml:
cargo install runner-run # or build locally: cargo build
runner lsp # speaks LSP over stdioIt provides, reusing the same logic the CLI uses:
- diagnostics, the exact
runner config validatechecks (syntax, unknown keys, bad package-manager / runner / source labels, conflicting policies) plus deprecation hints, live as you type; - hover, section and field documentation, sourced from the JSON Schema;
- completion, section names, field names, and value sets (package managers,
the
[tasks]runner/PM/source labels, policy enums, booleans).
Point your editor's generic LSP client at runner lsp for files named
runner.toml. Example (Neovim):
vim.lsp.start({
name = "runner",
cmd = { "runner", "lsp" },
root_dir = vim.fs.dirname(vim.fs.find({ "runner.toml" }, { upward = true })[1]),
})For schema-only autocompletion without the server, the #:schema directive that
runner config init writes is enough for editors with a TOML language server.
runner detects and works with:
npm, yarn, pnpm, bun, cargo, deno, uv, poetry, pipenv, go, bundler, composer
It aggregates tasks from these runners:
turbo, nx, make, just, go-task, mise, bacon
reading them from:
package.json / package.json5 / package.yaml
turbo.json / turbo.jsonc
deno.json / deno.jsonc
Makefile
justfile
Taskfile
bacon.toml
mise.toml / .mise.toml
Cargo aliases from .cargo/config.toml
pyproject.toml [project.scripts] (run via uv / poetry / pipenv)
It also understands monorepo/workspace context from:
turbo, nx, pnpm-workspace.yaml, npm/yarn/bun workspaces, lerna.json,
deno.json workspace, Cargo workspaces
From the workspace root, every member's package.json scripts and deno.json
tasks are listed and runnable:
$ runner list
package.json rfc:site
package.json rfc:typecheck
package.json @acme/web:site
cargo test
$ run rfc:site # member by manifest name, path, or directory name
$ run apps/web:site
$ run typecheck # bare name: only `rfc` defines it, so it runs there
$ run site # error: `rfc` and `@acme/web` both define itEvery directory beneath the workspace root sees the whole workspace. Inside a
member, that member's tasks come first and complete bare; the root's tasks are
next; other members stay member:task. A root task a member shadows is
reachable as root:task. Local-file tokens (./gen.sh) and package-manager
exec fallbacks still resolve against the directory you are in.
$ cd rfc
$ runner list
package.json site # rfc's
package.json typecheck
package.json @acme/web:site
cargo test # the root'sA root task always wins a bare name over another member's same-named task. A
bare name that several members define is refused with the qualified spellings.
Member tasks run in the member's directory through the package manager the
root resolves to. doctor --json and why --json address every task as
<scope>:<source>#<task> with scope being root or the member name, and
that form works from any directory in the workspace.
Support notes
nx is currently detection-only. runner uses it for project context, but does
not extract Nx tasks as direct task entries yet.
When multiple sources define the same task, runner chooses deterministically: turbo tasks first, then package manifest scripts, then other matching sources.
Workspace members contribute their own manifest scripts (package.json,
package.json5, package.yaml) and deno.json tasks. Other sources
(Makefile, justfile, …) are read from the root only. A member whose name
collides with a source label (just, make, deno, …) must be addressed by
path.
run <TAB>task completion across projects- One command shape across many ecosystems
- Simple CI with
runner install --frozenplusrun <task>steps - First-class GitHub Actions install step
- Automatic toolchain detection
- Task aggregation from common config files
- Task-first execution with command fallback
- Monorepo/workspace awareness
- Safe clean defaults
- Node version mismatch warnings
- Site: runner.kjanat.dev
- npm:
runner-run - crates.io:
runner-run - aur:
runner-run,runner-run-bin - docker:
ghcr.io/kjanat/runner,kjanat/runner
MIT © 2026 Kaj Kowalski