rite
An idempotent task runner with Unix-native variable precedence.
Status: v1.0.7 shipped. See CHANGELOG.md for per-release notes and RELEASING.md for the cut procedure.
- Binary builds, test suite green on Linux / macOS / Windows × Go 1.26.
- SPEC's 7-tier variable precedence and
${VAR} shell-native preprocessor are live.
rite --migrate <path> converts a Taskfile.yml → Ritefile.yml, walks includes: recursively, and flags anything that changes meaning under rite's semantics.
- All six legacy special vars (
.TASK, .TASK_DIR, .TASKFILE, .TASKFILE_DIR, .ROOT_TASKFILE, .TASK_VERSION) are rewritten at migrate time and runtime-aliased for Ritefiles that predate the rename.
includes: paths are sandboxed to the Ritefile tree; remote URLs, ../ escape, and symlink escape are rejected.
- Remote-Ritefile experiment removed — see §Non-goals.
- Public API locked at v1.0.0:
Ritefile* / Rite* types and exit-code numbers stand as the closed contract. Patch and minor releases ship fixes + additions; breaking changes call themselves out in the CHANGELOG.
- Docs: clintmod.github.io/rite · design contract in
SPEC.md · full release log in CHANGELOG.md.
Install
Homebrew (macOS / Linux):
brew install clintmod/tap/rite
Install script (macOS / Linux / FreeBSD / WSL):
curl -sSL https://raw.githubusercontent.com/clintmod/rite/main/install.sh | sh -s -- -b ~/bin
Downloads the latest release archive, verifies its SHA-256 against rite_checksums.txt, and drops rite into ~/bin. Pass a tag as the last argument to pin a version (… | sh -s -- -b ~/bin v1.0.7). Default bindir is ./bin if -b is omitted.
mise:
# mise.toml
[tools]
"ubi:clintmod/rite" = "v1.0.7"
(Older mise? See getting-started for the go: fallback.)
From source (Go 1.26+):
go install github.com/clintmod/rite/cmd/rite@latest
Binary download: releases page — darwin / linux / windows / freebsd × amd64 / arm64 / arm / 386 / riscv64, plus deb / rpm / apk packages.
Use
rite --init # writes Ritefile.yml
rite <task> # runs a task
rite --list-all # show all tasks
rite --migrate Taskfile.yml # convert a go-task Taskfile to a Ritefile
The five-second mental model: variables are first-in-wins. Shell env beats CLI args beats Ritefile defaults. Task-scope vars: are defaults only; if any higher tier sets the name, the task value is ignored.
What is this?
rite is a task runner in the same space as make, just, and go-task — you describe tasks in a declarative file, and the tool runs them with dependency resolution, parameters, and shell invocation.
The thing that makes rite different is how it handles variables. In one sentence: the value you set closest to the user wins. Your shell environment overrides everything. Your CLI arguments override the Ritefile. Internal vars: blocks declare defaults, not mandates.
This is how Unix has worked for 50 years. It is not how go-task works.
Why the name?
A rite is a ritual — a prescribed set of actions performed the same way every time. That's exactly what a task runner is: a script of steps you repeat on every build, every deploy, every release. The word also reads as a near-homophone of right, which fits the project's thesis — variables should behave the way Unix has always done it, i.e. the right way. Short, typable, a nod to task's spiritual ancestor rake, and doesn't collide with anything on PATH.
Why does this exist?
rite began as a hard fork of go-task/task. The upstream project has a variable model where a task's own vars: block overrides CLI arguments and shell environment — the inverse of every Unix precedent. A decade of bugs trace to this choice, and the upstream's proposed redesign preserves the inversion.
rite takes the opposite position: variable precedence should be first-in-wins, scoped sandboxes should be real, and dynamic variable evaluation should be pure.
See SPEC.md for the full design contract, including:
- The 7-tier variable precedence model
- Scoping rules for included Ritefiles
- Dynamic (
sh:) variable semantics
vars / env unification
- Template syntax (
${VAR} primary, Go-template secondary)
- File format (
Ritefile)
- Compatibility with
go-task (none — rite migrate converts one-way)
Relationship to go-task
rite imports go-task's git history to preserve attribution under its MIT license, but rite is not a compatibility fork:
- Different binary (
rite, not task)
- Different file format (
Ritefile, not Taskfile.yml)
- Incompatible variable semantics
- No intention to merge upstream changes that conflict with the SPEC
- One-way migration tool only
The original project is excellent software with a design choice its creators do not want to revisit. rite exists for users who want the different choice.
Non-goals
Remote Ritefiles. Ritefiles must be checked into the project they build. Fetching them over HTTP or git at runtime breaks idempotency — a build that depends on a remote URL is not self-contained, can silently change behavior between runs, and introduces a network dependency into what should be a deterministic local workflow. If you want to share task definitions across repos, vendor them (submodule, subtree, copy, or a generator script). includes: accepts local paths only, and any entrypoint containing :// is rejected.
License
MIT. See LICENSE. Original copyright © 2016 Andrey Nering; fork contributions © 2026 Clint Modien.
Roadmap
- Phase 0: Repo set up, spec drafted.
- Phase 1: Rebrand — module path, binary name, file format discovery.
- Phase 1.5: Cosmetic polish — log prefix, error strings,
rite --init.
- Phase 2: First-in-wins
getVariables(), per-resolution dynamic-var cache.
- Phase 3: Test fixture audit and rewrite; include-site var precedence fix.
- Phase 4:
${VAR} preprocessor, export: false opt-out, vars/env unified.
- Phase 5:
rite migrate tool, docs site, v0.1.0 release with Homebrew tap + mise support.
- 1.0 prep: CHANGELOG, Migrating-from-go-task guide, remote-Ritefile removal, N-deep include env-export fix, full special-var rewrite/alias coverage, Go 1.25 dropped from CI.
- v1.0.0: public API rename (
Task* → Ritefile*/Rite*), rite --migrate flag, schema-version upper bound, includes sandboxing (rejects ://, ../, symlink escape; redacts parse-error snippets from non-Ritefile targets), and concurrency hardening (Vars.Merge lock, signal-handler ctx cancel, templater.Cache race). The closed SemVer contract; see CHANGELOG.md.
- v1.0.3: migrate clobber fix (#76), template modernization (#74), docs audit cleanup, security hardening — see
CHANGELOG.md for the full list.
- v2.0 (planned): drop the
env: block. Phase 4 already unified vars: and env: into a single variable table with shared precedence; env: has been a vestigial synonym ever since. Keeping it around is pure ceremony and the direct cause of ambiguity bugs like #129 (same name in both blocks at the same scope). In 2.0, only vars: is accepted; export: false remains the opt-out for secrets. Migrate will fold existing env: entries into vars: one-to-one. SPEC simplifies to "there is one variable table, full stop."
Migrating from go-task
rite is an intentional semantic break from go-task, not a drop-in replacement. The five user-visible changes:
- Task-scope
vars: are defaults only. If the entrypoint sets FOO, a task-scope FOO is ignored. Upstream's last-in-wins model is inverted.
- Task-scope
env: is also defaults only. Same rule, applied to env blocks.
- Task-level
dotenv: files don't override entrypoint env. Same rule.
vars: auto-exports to the cmd shell environ. Add export: false on any var holding a secret that shouldn't leak.
- Shell env always wins over Ritefile env: SPEC tier 1 has no opt-out.
Run rite --migrate <path/to/Taskfile.yml> and it will: (a) write a Ritefile.yml with include-paths and special-var references rewritten, and (b) emit warnings to stderr for every site where the old and new meanings differ (OVERRIDE-VAR, OVERRIDE-ENV, DOTENV-ENTRY, SECRET-VAR, TEMPLATE-KEPT). The old SCHEMA-URL warning is gone — migrate now rewrites the # yaml-language-server directive in place instead of flagging it.