website/blog/2026-07-05-moon-v2.4.mdx
This release is all about control and trust. We're giving tasks a way to gate, skip, and fingerprint themselves with plain shell scripts, letting projects set default task options in one place, and rounding out the toolchain lineup with Ruby and Poetry support. Behind the scenes, the daemon and VCS layers also got a substantial hardening pass so moon behaves reliably in the messy edge cases real repositories throw at it.
<!--truncate-->Tasks often need to answer a question before (or instead of) actually running: "is this tool
installed?", "should this even run right now?", "did the environment change since last time?".
Previously you'd have to bake that logic into the task's command itself. In v2.4, we're
introducing a checks setting for tasks that runs shell scripts before the task, and reacts to
the result based on the type of check.
tasks:
deploy:
command: './deploy.sh'
checks:
# A plain string is shorthand for a `requirement` check
- 'command -v aws'
# Skip the task entirely if the condition script exits successfully
- check: 'condition'
script: './scripts/already-deployed.sh'
# Fold the script's stdout into the task's cache hash
- check: 'fingerprint'
script: 'openssl version'
hash: stdout
A requirement check runs a script, and if it fails, the task fails and does not run at all. This
is the default behavior when you provide a plain string instead of an object, making it the
quickest way to assert a precondition, like a required binary being on PATH.
A condition check is the inverse: if all condition scripts in the list pass, the task is
skipped instead of running. This is useful for idempotency checks, such as "has this migration
already been applied?" or "is the output already up to date?", where running the task again would
be redundant or wasteful.
A fingerprint check always runs alongside the task, but instead of gating it, its output is mixed
into the task's cache hash. This means a change in the script's output — like a different tool
version — invalidates the cache, even when none of the task's declared inputs changed. You can
control exactly what gets hashed (exit-code, stdout, stderr, or all output) via the hash
field.
Checks are inherited like any other task setting, and a new mergeChecks task option controls
whether inherited checks are appended, prepended, replaced, or preserved.
Task options have always been configurable per task, but if you
wanted the same option (say, disabling the cache, or bumping the retry count) applied across every
task in a project, you had to repeat it on each one. v2.4 adds a top-level taskOptions setting to
moon.* that defines defaults for every task in that project, which individual tasks can still
override.
taskOptions:
retryCount: 2
cache: false
tasks:
test:
command: 'jest'
# Inherits retryCount: 2 and cache: false
lint:
command: 'eslint .'
options:
cache: true # Overrides the project-level default
Ruby joins the toolchain lineup as an unstable, experimental integration. Like our other toolchains, it supports the full range of tiers — from basic language detection in tier 1, up through building from source in tier 3.
Configure it via unstable_ruby in
.moon/toolchains.*. Major thanks to @lamalex for the contribution!
unstable_ruby: {}
Run moon toolchain info unstable_ruby for the full list of available settings.
The Python toolchain now supports Poetry as a package manager,
alongside the existing pip and uv support, and works across tiers 1 through 3. Opt in by setting
packageManager: 'poetry', and fine-tune behavior through the new unstable_poetry settings.
unstable_python:
packageManager: 'poetry'
unstable_poetry: {}
Run moon toolchain info unstable_poetry for the full list of available settings.
A new internal storage layer landed this release, unifying how local and remote caches interoperate:
moon's background daemon powers a lot of the persistent, cross-run functionality. In v2.4 we reworked how the daemon establishes and maintains its identity so it behaves correctly under real-world churn:
daemon.json state file (replacing moond.pid).moon upgrade doesn't keep serving the old
binary.Git interactions got a hardening pass across the board:
View the official release for a full list of changes.
--update-constraint option to moon upgrade that updates the workspace config's version
constraint to the latest version after upgrading.--log level handling: debug covers most day-to-day debugging needs, while trace now
includes significantly more detail, geared towards agents and deep diagnostics.