Documentation is currently for moon v2 and latest proto. Documentation for moon v1 has been frozen and can be found here.
Migrate to moon v2.0
To ease the migration process from moon v1 to v2, we've compiled a list of all breaking changes and important changes that you should be aware of. Please read through these carefully before upgrading your workspace.
To automate some of the migration process, we've created the moon migrate v2 command that will
migrate all applicable settings in configuration files.
$ moon migrate v2
CLI
- Removed
x86_64-apple-darwin(Apple Intel) as a supported operating system. Onlyaarch64-apple-darwin(Apple Silicon) is now supported. - Removed canary and nightly releases.
Commands
- We've done a large polish pass for all commands, based on the CLI guidelines.
- Updated the
moonxbinary to usemoon execinstead ofmoon rununder the hood. - Renamed all options and flags to kebab-case instead of camelCase.
- Example:
--logLevel->--log-level
- Example:
- Renamed options for all commands:
--platform->--toolchain
- Removed commands:
moon nodemoon migrate from-package-json(use themigrate-turborepoextension instead)moon query hash(usemoon hashinstead)moon query hash-diff(usemoon hashinstead)
moon action-graph
- Changed the output of
--jsonto a new JSON structure (now matches the project and task graphs).
moon check
- Now runs
moon execunder the hood, with some arguments/options pre-filled. - If the project ID is not specifed, it will no longer find the closest project. Instead you must
pass
--closest. - Renamed options:
--update-cache, -u->--force, -f
moon ci
- Now runs
moon execunder the hood, with some arguments/options pre-filled. - No longer includes graph relations when determining affected state, and solely relies on changed
files/env vars. Pass
--include-relationsto include them. - Renamed options:
--update-cache, -u->--force, -f
moon generate
- Changed the destination from a positional argument, to the
--tooption.- Example:
moon generate <id> ./dist->moon generate <id> --to ./dist
- Example:
moon init
- Removed "scaffolding a toolchain" functionality from the command. Use the
moon toolchain addcommand instead. - Removed options:
--to(use a positional argument instead)
moon mcp
- Updated protocol version to 2025-11-25.
- Updated the
get_projectstool to no longer have anincludeTasksoption. - Updated the
get_projectstool to return a list of project fragments, instead of the whole project object. This was required as the response was too large for MCP. - Updated the
get_taskstool to return a list of task fragments, instead of the whole task object. This was required as the response was too large for MCP.
moon query projects
- Removed options:
--dependents
moon run
- Now runs
moon execunder the hood, with some arguments/options pre-filled. - No longer includes graph relations when determining affected state, and solely relies on changed
files/env vars. Pass
--include-relationsto include them. - Updated options:
--dependentsnow requires a value, eitherdeepordirect
- Renamed options:
--update-cache, -u->--force, -f
- Removed options:
--no-bail(usemoon execinstead)--profile--remote(use--affected remoteinstead)
moon templates
- Changed the output to render a table instead of a list.
moon run
- Running a target without a scope no longer locates the closest project and instead uses the new
default project feature. To run a target in the closets project, use
~:scope instead.- Example:
moon run build->moon run ~:build
- Example:
Workspace
Configuration: .moon/workspace.*
- Renamed setting values:
codeowners.orderByvalueproject-name->project-id
- Renamed settings:
codeowners.syncOnRun->codeowners.syncconstraints.enforceProjectTypeRelationships->constraints.enforceLayerRelationshipsdocker.prune.installToolchainDeps->docker.prune.installToolchainDependenciesdocker.scaffold.include->docker.scaffold.configsPhaseGlobsrunner->pipelineunstable_remote->remotevcs.manager->vcs.clientvcs.syncHooks->vcs.sync
- Removed settings:
docker.scaffold.copyToolchainFilesexperiments.*hasher.batchSizepipeline.archivableTargets
Toolchains
Configuration: .moon/toolchain.*
- This file was renamed to
.moon/toolchains.*(plural) to reflect that multiple toolchains are configured. This also aligns with the new.moon/extensions.*file. - All toolchains have been stabilized (excluding Python), so the
unstable_prefix must be removed from identifiers.
JavaScript
The bun, deno, and node toolchains now require the javascript toolchain to be defined as
well. All shared settings have been moved to the javascript toolchain.
In addition, all node package managers are no longer nested under the node toolchain, but are
now top-level settings. These are only required when the javascript.packageManager setting is
defined.
- Moved settings:
bun.dependencyVersionFormat->javascript.dependencyVersionFormatbun.inferTasksFromScripts->javascript.inferTasksFromScriptsbun.rootPackageOnly->javascript.rootPackageDependenciesOnlybun.syncProjectWorkspaceDependencies->javascript.syncProjectWorkspaceDependenciesnode.dependencyVersionFormat->javascript.dependencyVersionFormatnode.dedupeOnLockfileChange->javascript.dedupeOnLockfileChangenode.inferTasksFromScripts->javascript.inferTasksFromScriptsnode.packageManager->javascript.packageManagernode.rootPackageOnly->javascript.rootPackageDependenciesOnlynode.syncPackageManagerField->javascript.syncPackageManagerFieldnode.syncProjectWorkspaceDependencies->javascript.syncProjectWorkspaceDependenciesnode.bun->bunnode.npm->npmnode.pnpm->pnpmnode.yarn->yarn
- Renamed settings:
node.binExecArgs->node.executeArgs
- Removed settings:
bun.packagesRootdeno.depsFiledeno.lockfilenode.addEnginesConstraintnode.packagesRoot
# Before
node:
version: '22.14.0'
packageManager: 'yarn'
inferTasksFromScripts: false
syncPackageManagerField: true
syncProjectWorkspaceDependencies: true
yarn:
version: '4.8.0'
# After
javascript:
packageManager: 'yarn'
inferTasksFromScripts: false
syncPackageManagerField: true
syncProjectWorkspaceDependencies: true
node:
version: '22.14.0'
yarn:
version: '4.8.0'
Python
All the Python toolchains are still unstable, and still require the unstable_ prefix. We're not
Python experts, so we would love help from the community to test and improve the implementation. If
you're interested in helping out, please reach out in our Discord server.
- Moved settings:
python.pip->unstable_pippython.uv->unstable_uv
- Removed settings:
python.rootVenvOnly
# Before
python:
version: '3.12.0'
packageManager: 'uv'
uv:
version: '0.10.2'
# After
unstable_python:
version: '3.12.0'
packageManager: 'uv'
unstable_uv:
version: '0.10.2'
Extensions
Configuration: .moon/extensions.*
- The
extensionssetting from.moon/workspace.*has been moved (and flattened) to its own file,.moon/extensions.*. - The built-in extensions
download,migrate-nx, andmigrate-turborepomust now be enabled in the configuration file before they can be used. Simply set an empty object.- This change was made to reduce the number of extensions that are loaded by default, improving performance.
download: {}
migrate-nx: {}
Pipeline
Affected graph relations
In v1 when determining the affected state of a task (via --affected), the graph relations
(dependencies and dependents) were always included in the calculation. This meant that tasks were
affected, even if there were no changed files or environment variables, just because they were
related to other affected tasks. This was sometimes confusing to users, as it seemed like the
pipeline was over-zealously running tasks that it shouldn't (even though it was correct).
In v2 and later, the graph relations are no longer included by default, and only changed files and
environment variables are taken into account. To include graph relations in the affected
calculation, you can pass the --include-relations (-g) flag.
Better CI handling
Tasks have an option called runInCI that indicates whether the task should be run in CI or not,
and can be configured with more granulary conditions. However, in v1 this option was only taken into
account in the moon ci command, and not in moon run. There was no way to force this option to be
respected in moon run, which meant that users had to be careful to not accidentally run CI-only
tasks locally.
In v2, we've improved the implementation of this option, and how we detect CI environments. The following changes have been made.
moon ci,moon check,moon run, andmoon execnow all respect therunInCIoption.moon ciis always forced in CI mode. The other commands are dependent on theCIenvironment variable.- To force CI mode, set the
CIenvironment variable totrue, or pass the--ciflag.
- To force CI mode, set the
- The
runInCIoption will be applied when in an CI environment. The exception to this is the "only" condition, which also applies locally. - The
runInCIoption can be disabled entirely withmoon exec --ignore-ci-checks.
Project
Configuration: moon.*
- Renamed settings:
docker.scaffold.include->docker.scaffold.sourcesPhaseGlobsproject.name->project.titletype->layertoolchain->toolchainsplatform->toolchains.default
- Removed settings:
Language detection
The primary language is now detected from toolchains, instead of being a hardcoded implementation.
The result may now differ, as the first toolchain in the list will be used. Additionally, languages
that don't have a toolchain yet, like PHP or Ruby, will not be detected and must be explicitly
configured.
# After
language: 'ruby'
Custom metadata
The project.metadata setting has been removed, but all custom metadata fields can now be defined
at the root of the project setting.
# Before
project:
metadata:
customField: 'value'
# After
project:
customField: 'value'
Toolchain disabling
The toolchain.*.disabled setting was removed. Instead set the toolchain itself to null/false.
# Before
toolchain:
typescript:
disabled: true
# After
toolchains:
typescript: null
Tasks
Configuration: moon.*, .moon/tasks
- The file
.moon/tasks.ymlhas been removed. If you want to support tasks that are inherited by all projects, then move this to.moon/tasks/all.ymland do not configure the newinheritedBysetting. - Renamed tokens:
$projectName->$projectTitle$projectType->$projectLayer$taskPlatform->$taskToolchain
- Renamed settings:
tasks.*.local->tasks.*.presetusingservervaluetasks.*.platform->tasks.*.toolchainstasks.*.options.affectedPassInputs->tasks.*.options.affectedFiles.passInputsWhenNoMatch
- Removed setting values:
tasks.*.presetvaluewatcher
- Changed settings:
tasks.deps.*.argsno longer supports a string. Use a list of strings instead.tasks.*.options.affectedFilesnow supports an object for more granular control.
- Changed option defaults:
tasks.*.options.envFilenow defaults to a list of files, instead of a single file, whentrue. Refer to the blog post for more information.tasks.*.options.inferInputsnow defaults tofalseinstead oftrue.tasks.*.options.shellnow defaults totrueinstead offalse.tasks.*.options.unixShellnow defaults tobashinstead of nothing.tasks.*.options.windowsShellnow defaults topwshinstead of nothing.
Simple commands only
We've reworked command (and args) to only support simple commands. A simple command is an
executable (binary, file, etc) followed by zero or many arguments.
Complex commands that involve shell features like piping (|), redirection (>, <), chaining
(&&, ||, ;), and environment variable assignment (KEY=value) are no longer supported
directly in the command setting. Use script instead.
# Before
tasks:
example:
command: 'echo "foo" | grep "f"'
# Before
tasks:
example:
script: 'echo "foo" | grep "f"'
Shell features like expansion, globbing, and substitution is still supported in
command.
Local mode
The local task setting has been removed as the name was confusing. Users assumed it meant "only
run locally", but it actually meant "this is a persistent server that should only run locally".
Instead, use the preset setting with a value of server.
# Before
tasks:
dev:
command: 'start-dev'
local: true
# Before
tasks:
dev:
command: 'start-dev'
preset: 'server'
If you want a task that is simply "local only" without other options changes, use options.runInCI
directly.
tasks:
dev:
command: 'start-dev'
options:
runInCI: false
Shells by default
Tasks now run in a shell by default, and will use Bash on Unix (options.unixShell), and pwsh on
Windows (options.windowsShell). You can disable this behavior by setting options.shell to
false.
tasks:
dev:
command: 'start-dev'
options:
shell: false
Env var substitution behavior
The syntax and behavior for substituting (expanding/interpolation) environment variables has
changed, to better align with the standard of .env files. The biggest change is that flagless
tokens ($VAR) and ? flag tokens ($VAR?) swapped functionality. Refer to the following table:
| Syntax | v1 | v2 |
|---|---|---|
$VAR | Substitute with variable syntax ($VAR) if variable empty | 💥 Substitute with empty string if variable empty |
$VAR! | Don't substitute and keep variable syntax ($VAR) | 💥 Removed syntax |
$VAR? | Substitute with empty string if variable empty | 💥 Removed syntax |
${VAR} | Substitute with variable syntax ($VAR) if variable empty | 💥 Substitute with empty string if variable empty |
${VAR!} | Don't substitute and keep variable syntax ($VAR) | ~ |
${VAR?} | Substitute with empty string if variable empty | 💥 Substitute with variable syntax ($VAR) if variable empty |
${VAR:default} | Use default value if variable empty | ~ |
${VAR:-default}, ${VAR-default} | ⛔️ Not supported | Use default value if variable empty |
${VAR:+alternate}, ${VAR+alternate} | ⛔️ Not supported | Use alternate value if variable non-empty |
Legend:
~indicates the syntax/functionality is the same as v1💥indicates breaking change⛔️indicates not supported
Env var precedence
The order of precedence for environment variables has slightly changed when running tasks, as it was confusing to users. The new order of precedence is as follows, from lowest to highest tier, with the latter overwriting the former:
- Task
.envfiles- via
options.envFile
- via
- Task env variables
- via
envordeps.*.env
- via
- System variables
- via profile scripts (
.bashrc,.zshrc, etc) - via command line:
KEY=value moon ...
- via profile scripts (
In regards to variable substitution, each tier can reference variables within the same tier, or the higher tier(s), but not from lower tier(s). This is because variables are processed in reverse order. Refer to the following table:
| Tier | Can reference | Evaluated during |
|---|---|---|
| Dotenv | Dotenv, Task, System | Before task execution |
| Task | Task, System | After task creation |
| System | System | CLI startup |
If you don't want to inherit a system variable, you can override it in the task with a null value.
tasks:
dev:
env:
EXAMPLE: null
Deferred .env files
In v1, when options.envFile was enabled, the .env file(s) were loaded at the time of task
creation, during the building of project/task graphs. This meant that if the .env file changed
between the time of graph creation and task execution, the changes would not be reflected.
In v2 and later, .env files are loaded just before task execution, ensuring that any changes to
the file are picked up.
Because of this change, task env variables will continue to override .env file variables, BUT
can no longer reference them for substitution. This is because the .env files are loaded later in
the process.
Toolchain detection
How toolchains work and get detected for projects and tasks has been heavily reworked in v2.
Ideally, you should never configure the toolchains setting for a task directly. This can cause
unintended consequences, and instead you should rely on the toolchain detection system itself.
For example, in moon v1, the platform: node setting represented Node.js, the configured package
manager (npm, pnpm, yarn), and JavaScript itself. That's 3 different layers of functionality in 1
implementation. In v2, these layers are now split into separate toolchains (plugins), and act
independently.
So if you had the following configuration in v2:
tasks:
build:
# ...
toolchains: 'node'
This would only apply the Node.js toolchain, and NOT the JavaScript toolchain, NOR the package manager toolchain. This may cause unexpected issues. The correct configuration should be:
tasks:
build:
# ...
toolchains: ['node', 'javascript', 'npm']
But adding all those toolchains is cumbersome, so we suggest omitting toolchains entirely and let
the detection do its thing.
Other changes
- When
options.affectedFilesis enabled, the list of files will be joined with the OS path separator (:on Unix,;on Windows) instead of a comma (,) when passed as theMOON_AFFECTED_FILESenvironment variable.
Task inheritance
Deep merged instead of shallow merged
In v1, when inheriting tasks, all global configs (those in .moon) were shallow merged into a
single config ignoring merge task options, and then merged with the local config (moon.*) using
merge task options. This was not intuitive, as users expected all configs to be merged in sequence.
To demonstrate this, take the following example configs, in order of inherited:
# .moon/tasks.yml
tasks:
build:
command: 'build --cache'
options:
mutex: 'build'
# .moon/tasks/tag-a.yml
tasks:
build:
args: '--force'
# .moon/tasks/tag-b.yml
tasks:
build:
args: '--clean'
Users would expect the final build task to be build --cache --force --clean with the mutex
option set. However, since the tasks setting was shallow merged, only the last config (tag-b)
would be used, resulting in the task being noop --clean without the mutex. The noop pops up
because the command setting was not configured in the last config.
To remedy this, and to improve task composition overall, global configs are no longer shallow merged into a single config before merging with the local config. Instead, all configs are merged in sequence, while respecting the task merge options. This is the order of operations for the new system:
- Load all global configs (
.moon) into a list, in order, based on theinheritedBysetting. - Load the local config (
moon.*). - Create an inheritance chain by resolving global configs first, then the local config last, while
respecting
extendand other composition settings. - Merge all task options in order, to create the final task options.
- Merge all tasks in order, using the final task options to guide the merging behavior.
File groups are merged
Because of the new deep merging behavior, file groups defined in inherited configs are now merged together, instead of being replaced by the following config in the sequence. For example, given the configs:
# .moon/tasks/tag-a.yml
fileGroups:
sources:
- 'src/**'
# .moon/tasks/tag-b.yml
fileGroups:
sources:
- 'docs/**'
In v1, the final sources file group would only include docs/**, as the second config would
replace the first. In v2, the final sources file group includes both src/** and docs/**.
VCS
New hooks system
We've rewritten our Git hooks from the ground up to be based around the core.hooksPath setting.
The following changes have been made:
- We no longer write hooks to the
.git/hooksdirectory. - Instead, all hooks are written to
.moon/hooksand Git is configured to use this directory. - Bash scripts no longer end in
.sh.
We currently don't have an easy way to clean the previous implementation of hooks. You may need to
manually remove the old scripts from .git/hooks and .moon/hooks if they are causing issues.
Other changes
Changed (touched) files
We renamed the terminology "touched files" to "changed files" throughout the codebase and documentation. This better aligns with common VCS terminology and reduces confusion. Because of this, the following changes were made:
- Renamed the
moon query touched-filesCLI command tomoon query changed-files. - Renamed the
get_touched_filesMCP tool toget_changed_files. - Renamed the
touchedFilesrun report field tochangedFiles.
# Before
$ moon query touched-files
# After
$ moon query changed-files
Docker
- The scaffolded
.moon/docker/workspacedirectory was renamed to.moon/docker/configs. - The
moon docker filecommand will now loop through all toolchains and use the first image found, otherwise it defaults to "scratch". If you want to be explicit, set thedocker.file.imagesetting.
Query language (MQL)
- Renamed fields:
projectName->projectIdprojectType->projectLayertaskPlatform->taskToolchain
# Before
projectType=application && taskPlatform=node
# After
projectLayer=application && taskToolchain=node
Webhooks
- Removed the
tool.*events. Usetoolchain.*events instead. - Removed the
runtimefield fromdependencies.*events. Use thetoolchainfield instead.