Forest CLI Commands
The Forest CLI provides commands to help you manage your packages. This page is the full reference; to get the CLI itself, see Installing the Forest CLI.
| Command | What it does |
|---|---|
forest init | Start a new package (or project) in the current directory |
forest install | Install a package, or everything in your forest.json |
forest remove | Remove a package from your project |
forest update | Update dependencies to the newest versions your ranges allow |
forest audit | Check dependencies for updates and license considerations |
forest tree | Show the installed dependency tree |
forest link | Point a dependency at a local folder while you work on it |
forest unlink | Remove a local link and restore the registry version |
forest mount | List, create, rename, or remove the project’s dependency folders (mounts) |
forest publish | Publish your package to the registry |
forest login | Log in to your Forest PM account |
forest logout | Log out and clear stored credentials |
forest whoami | Show the currently logged-in user |
forest upgrade | Update the CLI itself to the latest release |
forest override | Force a transitive dependency onto a version range |
forest exclude | Ban versions of a package from being installed |
forest init
$forest initStarts development on a new package in the current directory.
On Roblox, init asks for a package name and the path to your root module (default src/init.luau), creates a starter module at the root path if the file doesn’t exist yet, and writes a forest.json with the name, a starting version (0.1.0), the platform, and the root path. Dependencies you install will live in a Packages/ folder next to your root module (src/Packages/ in the default layout): the same place they sit when a consumer installs your package, so your requires work identically in development and after install. Pass --packages-dir to rename that folder; the name is recorded as packagesDir in forest.json.
When the folder has a wally.toml, Roblox init offers to import it: [dependencies] become your dependencies, [server-dependencies] and [dev-dependencies] become ServerPackages and DevPackages mounts (Wally’s own folder names), and wally.lock is deleted. --project offers the same import, as does forest install when it creates the manifest. See Moving a project from Wally.
On UEFN, init infers what to scaffold from where you run it; see Authoring & Publishing.
$ forest init> Platform: <select your platform>> Package name: my-package> Path to the package's root file (created if missing): src/init.luau🌳 Initialized package "my-package"
-p--platform<platform>- Skip the platform picker:
--platform robloxor--platform uefn. --packages-dir<name>- Roblox only: install dependencies into
<name>instead of the defaultPackages, recorded aspackagesDirinforest.json. --project- Set up a game or project that only consumes packages: writes a bare manifest (platform and an empty dependency list) with a top-level
Packages/folder, no prompts.
forest install
$forest install [package-name]aliasesforest iforest growInstalls a package from Forest PM. Downloads the package and its dependencies into your project’s packages directory. Run without a package name to install everything in your forest.json.
Run in a folder with no forest.json, install offers to create the same bare manifest as forest init --project; pass --init <platform> to skip that prompt in scripts.
In a Roblox project with mounts, a new package goes to the default mount unless you pass --mount, and forest install with no package name installs every mount. If adding a package fails to resolve or install, forest.json is put back the way it was and the command exits with status 1. On Roblox, install keeps each dependency folder fully managed; see Forest owns its dependency folders.
-v--version<version>- Install a specific version instead of the latest.
-a--alias<alias>- Install the package under an alias.
-f--force- Reinstall everything from scratch, ignoring installed state.
--init<platform>- Create the bare manifest without prompting when there is no
forest.json, for use in scripts. --links<mode>- How to treat local links:
apply,ignore(install exactly what the lockfile says), orforbid(fail if any links are configured, for strict CI). The default isignoreon CI machines andapplyeverywhere else. --frozen- Install exactly what
forest-lock.jsonpins. Fails instead of updating the lockfile when it is missing or no longer matchesforest.json. For CI; see CI & API Tokens. -m--mount<mount>- Roblox: add the package to this mount instead of the default one. Without a package name, install only this mount.
Private packages need a login or an API token in the FOREST_TOKEN environment variable; see CI & API Tokens. If the registry refuses a private package in your tree (you’re not logged in, your login expired, or your account can’t read its scope), install skips it and installs everything else, then prints one error listing what it skipped and exits with status 1. A re-resolve keeps the version forest-lock.json pins for a private package you can’t read, with a warning. See Installing without access.
forest remove
$forest remove <package-name>aliasforest chopRemoves a package from your project and updates your forest.json. With mounts, it removes the package from the one mount that declares it; if several mounts declare it, pick one with --mount. If the reinstall fails, forest.json is put back.
-m--mount<mount>- Remove the package from this mount.
forest update
$forest updateMoves every dependency, direct and transitive, to the newest version its declared range allows. This is the safe, routine way to pick up fixes: forest.json is never touched, so a ^1.4.0 range can move to 1.6.2 but never to 2.0.0. The project is re-resolved fresh and reinstalled, and the command prints exactly which packages moved (per mount, when the project has mounts).
$ forest updateℹ forest/signal 2.4.0 -> 2.4.2ℹ forest/sauce 1.1.0 -> 1.2.0🌳 Updated 2 packages within the declared ranges!
To jump past your declared ranges (major versions), use forest audit --update instead, and read its report first. To update the CLI itself, see forest upgrade, which was called forest update before v1.11.
-m--mount<mount>- Only update this mount.
forest audit
$forest audit [package-name]aliasforest outdatedChecks your dependencies for available updates and license considerations. Where forest update stays inside your declared ranges, audit is how you review and cross them: read the report, then re-run with --update.
For each direct dependency, audit reports three versions:
- Current: the version pinned in your lockfile.
- Wanted: the highest version that satisfies the range declared in
forest.json. - Latest: the newest published version, even if your range doesn’t allow it.
After the version table, audit prints a license report over your whole resolved dependency tree (direct and transitive), flagging packages whose license safety rating deserves a look before you ship: each flagged version shows its license, a plain-language severity, and that version’s license caveats. Audit also reports any archived packages in your tree (no longer maintained, with the reason the owner left); installs warn about these with one consolidated line and point here for the details. Archived packages always keep installing, nothing is blocked. Finally, audit lists any overrides and exclusions declared in your manifest, warning about entries that no longer match anything.
Pass a package name to audit just that dependency. The name can even be a transitive dependency, in which case audit checks its installed license.
In a project with mounts, audit prints one version table per mount, and --update rewrites each range in the mount that declares it. When several mounts declare the package you name, --update needs --mount to pick one.
-u--update- Bump every outdated dependency’s range in
forest.jsonto^latest, then re-resolve and reinstall. Aliases are preserved. If the reinstall fails,forest.jsonis put back. -m--mount<mount>- Only audit this mount.
forest tree
$forest tree [package-name]aliasesforest lsforest listPrints the installed dependency tree, so you can see exactly which versions are in your project and which package pulled in which transitive dependency. Pass a package (scope/name, an alias, or a bare name that’s unambiguous) to show only its subtree. A project with mounts gets one tree per mount.
-m--mount<mount>- Only show this mount.
forest link
$forest link [path]Points one of your dependencies at a local folder, so you can iterate on a package inside a real project without publishing on every change. Roblox only for now.
The target folder is a normal package workspace with its own forest.json; Forest reads the package’s identity from it, so you never type a name that could mismatch the path. The package must already be a dependency of your project (a link overrides an installed dependency, it doesn’t add one), which also means it needs at least one published version. If several mounts declare it, pick one with --mount. Publish a 0.1.0 once, then link and iterate freely.
$ forest link ../signal-devℹ Added .forest/ to .gitignore (link state is machine-local and must not be committed).⚠ 1 package linked locally:⚠ forest/signal -> ../signal-dev (registry pin: 2.4.0, linked: 2.5.0-dev)🌳 Linked forest/signal -> ../signal-dev
While linked, the package’s folder in your Packages/ tree is a live view of the linked source (a directory junction on Windows, a symlink on macOS and Linux): edit a file there and Rojo, Studio, and luau-lsp see the change immediately, no reinstall needed. When a live link isn’t possible (some cloud-synced folders, or a package whose root module isn’t named init.*), Forest copies the folder instead and tells you so; in copy mode, re-run forest install after editing to refresh the copy.
Links are your machine’s state, not your project’s. They live in a gitignored .forest/links.json, and neither forest.json nor forest-lock.json changes: a teammate who pulls your commits gets a completely normal registry install. So you can’t forget a link is active, every install and forest tree prints a banner naming the linked packages, and forest link with no arguments lists them along with how the linked copy has drifted from the pinned registry version.
A few things to know while a link is active:
- The linked package’s own dependencies come from its own folder (run
forest installthere), not from your project’s tree. - CI ignores links by default, so a stray links file can never change what CI builds; see the
--linksflag onforest install. forest publishrefuses to publish your package while any of its dependencies is linked: the tree you tested isn’t the tree consumers would get. Unlink and re-test first.- The linked view exposes the package’s full working folder, including files that publishing would filter out. That’s expected, not a bug.
-m--mount<mount>- Link the dependency in this mount.
forest unlink
$forest unlink [package-name]Removes a local link (pass the package or the linked path) and restores the exact registry version your lockfile pins, re-verified from the local cache. Unlinking something that isn’t linked is a no-op, not an error.
$ forest unlink forest/signalℹ Unlinked forest/signal (was -> ../signal-dev)🌳 Restored 1 package from the registry.
--all- Remove every active link.
-m--mount<mount>- Only unlink in this mount.
forest mount
$forest mount [action]Manages a Roblox project’s mounts: extra dependency folders, each with its own dependency list, declared under mounts in forest.json. With no action (or list, alias ls), it lists every mount with how many dependencies it declares, the default mount first.
$ forest mount3 mounts:Packages (default) 2 dependenciesDevPackages 1 dependencyServerPackages 1 dependency
Wherever a command takes a <mount> (including every -m/--mount flag), give the mount’s path or enough of the end of it to be unique, compared case-insensitively: server/Packages finds src/server/Packages. A reference that matches several mounts is an error that lists them.
forest mount create
$forest mount create <path>Adds a mount at <path>, relative to forest.json (for example ServerPackages or src/server/Packages), and installs it. The folder’s name follows the same rule as packagesDir, and the path must stay inside the project and can’t overlap another mount or hold your root file. The folder can be new, empty, or one Forest or Wally installed packages into (Wally’s _Index and link modules are removed); a folder holding anything else is refused, because Forest owns everything in a mount. Afterwards, add packages with forest install <scope/name> --mount <path> and map the folder in your Rojo project.
forest mount rename
$forest mount rename <mount> <new-path>Moves or renames a mount’s folder in one step and updates forest.json and forest-lock.json to match. Nothing is reinstalled. The new path must not exist yet (an empty folder is fine). Code that requires the folder by name needs the new name, and your Rojo project needs the new $path.
The default mount stays where it is (next to forest.json, or next to your root module), so only its name can change: forest mount rename Packages Deps renames the folder and records "packagesDir": "Deps" (with a root module at src/init.luau, that’s forest mount rename src/Packages src/Deps). For a package you publish, consumers put its dependencies in a folder of that name too, starting with your next publish.
forest mount remove
$forest mount remove <mount>Deletes a mount’s folder, its entry in forest.json, its section in forest-lock.json, and any local links into it. It asks first, and the answer defaults to no. Without a terminal to ask in, it fails unless you pass --yes. The default mount can’t be removed.
-y--yes- Remove without the confirmation prompt.
forest publish
$forest publishPublishes your package to Forest PM. Uploads the package and its metadata to the Forest PM registry, making it available for others to install.
The first publish of a package asks for its visibility (unless forest.json sets visibility) and fills in anything missing, such as the description and license. For the version, publish uses the one in forest.json when it isn’t published yet (it asks you to confirm), and otherwise asks what kind of change you made and bumps it. The license question is skipped when your license file matches the license of the latest published version. Private packages don’t need a license file; with none, and no license in forest.json, the version is published as UNLICENSED.
On Roblox, only the default mount’s dependencies are published; extra mounts and forest-lock.json are left out of the archive.
Publishing needs your login: API tokens are read only, so forest publish refuses to run while FOREST_TOKEN is set.
-y--yes- Publish without prompts, reading everything from
forest.json. Implied whenCIis set.
Publishing without prompts
forest publish --yes asks nothing and reads every value from forest.json. It is also what you get when the CI environment variable is set to anything but 0 or false.
name,author,description, andversionmust be set. The version is published exactly as written, so it must be valid SemVer and not published yet; there is no bump question.- A new package needs
visibility("public"or"private"). An existing package keeps its visibility, and avisibilitythat disagrees with it is an error. - The license comes from the
licensefield, or from a license file Forest recognizes. Public packages need a license file and aREADME.md. - On Roblox, the root module must be set in
rootor found without asking.
Anything a prompt would have settled is an error instead, and every failure exits with status 1. It still needs a login stored by forest login on that machine, since API tokens can’t publish.
forest login
$forest loginLogs in to your Forest PM account. Two ways to authenticate:
- Browser (recommended): opens forest.dev in your browser; approve the request there and the CLI picks up your session automatically.
- Username/Password: prompts in the terminal. All Forest accounts use two-factor authentication, so you’ll be asked for a code from your authenticator app (or a backup code) after your credentials.
$ forest login? How would you like to log in? Username/Password> Username: your-username> Password: ********ℹ️ Two-factor authentication required> Authenticator code (or backup code): 123456🌳 Logged in as your-username
Browser sign-in waits about five minutes for you to approve, and prints the sign-in link in case the browser doesn’t open (over SSH, for example). A sign-in denied in the browser, or one that times out, exits with status 1.
forest logout
$forest logoutLogs out of your Forest PM account and clears the credentials stored on this machine.
forest whoami
$forest whoamiPrints the account you’re currently logged in as. When FOREST_TOKEN is set it describes that API token instead: its name, owner, access, and expiry.
forest upgrade
$forest upgradeUpdates the Forest CLI itself to the latest release (to update your project’s dependencies, see forest update). It downloads the correct binary for your platform, verifies it against the signed release manifest, and replaces the running executable in place, no reinstall needed.
$ forest upgrade🌳 Updated forest v1.11.0 -> v1.12.0 🌲
--check- Only report whether an update is available, without installing it.
Forest also shows a one-line notice when a newer version is available. That check runs at most once a day and is skipped in CI and non-interactive shells (or set FOREST_NO_UPDATE_CHECK=1 to disable it entirely).
If the CLI was installed with Rokit, Rokit owns the binary: forest upgrade refuses to run and points you to rokit update forest instead. The update notice still appears, throttled to once every 14 days since Rokit projects often pin an older version on purpose.
forest override
$forest override [package-name]Forces every transitive occurrence of a package (one you don’t declare yourself) onto a version range of your choosing, recorded in the overrides map of your forest.json. The forced range replaces whatever range each dependent declared, in either direction: pull a stuck dependency forward, or hold it back past a broken release until upstream catches up. Run with no package name to list the overrides currently declared.
Direct dependencies are rejected: their range already lives in dependencies, so change it with forest install <scope>/<name> -v <version> instead. To ban specific versions of any dependency, direct or transitive, use forest exclude.
The interactive wizard validates your range against the package’s published versions (skipping any that your excludes ban) and shows the exact version it resolves to before asking you to confirm. The project is then re-resolved and reinstalled immediately; if resolution fails, forest.json is rolled back untouched and the command exits with status 1.
$ forest override forest/signal> New SemVer range for forest/signal (current: installed 2.4.0): ^2.5.0ℹ Override version will be v2.5.1 (currently 2.4.0).? Accept? Yes🌳 Override set: forest/signal -> ^2.5.0
While an override is active, every install reports how many dependency edges it rewrote (run forest tree to see the affected packages tagged in place), warns if the override matches nothing in the tree, and notes when it is no longer needed because dependencies already resolve inside it on their own.
Overrides apply only to your own project: forest publish does not carry them to consumers. They apply to every mount in it.
-r--range<range>- The range to force, skipping the interactive prompt. Fails if no published version satisfies it.
-y--yes- Apply without the confirmation prompt.
--remove- Remove the override for this package.
forest exclude
$forest exclude [package-name]Bans a range of a package’s versions from ever being installed, recorded in the excludes map of your forest.json. Unlike an override, an exclusion applies to direct and transitive dependencies alike: the banned versions are removed from every candidate set, declared ranges are still honored across the versions that remain, and resolution fails loudly (naming the exclusion) if a declared range has no allowed version left. Run with no package name to list the exclusions currently declared.
Before you confirm, the wizard shows exactly which published versions the ban covers, and warns if your range has no upper bound, since that also bans every future version, including the eventual fix. Prefer banning the known-bad set (for example =1.6.0). A range that bans nothing, or that bans every published version, is rejected. As with overrides, the change reinstalls immediately, and if resolution fails it rolls back forest.json and exits with status 1.
$ forest exclude user1/sauce> Versions of user1/sauce to exclude (e.g. =1.6.0): =2.2.0ℹ Bans 1 published version: 2.2.0.ℹ Currently installed 2.2.0 will be replaced on reinstall.? Accept? Yesℹ user1/sauce: 2.2.0 -> 2.1.4🌳 Exclusion set: user1/sauce -> =2.2.0
Installs warn when an exclusion matches nothing in your tree, and let you know once it stops affecting resolution (every range now picks an allowed version on its own, typically because a fixed release shipped), so stale entries don’t linger unnoticed.
Exclusions apply only to your own project: forest publish does not carry them to consumers. They apply to every mount in it.
-r--range<range>- The versions to ban (e.g.
=1.6.0), skipping the interactive prompt. -y--yes- Apply without the confirmation prompt.
--remove- Remove the exclusion for this package.
Exit codes
As of CLI 1.15.0, these failures exit with status 1, so scripts and CI can rely on the result:
forest install, when adding a package fails (forest.jsonis put back) or when private packages were skipped.forest publish, on any failure. Cancelling at a prompt is not a failure.forest overrideandforest exclude, when the change can’t be applied (forest.jsonis rolled back).forest login, when the browser sign-in is denied or times out.