Board the newest train
$ grim update --dry-run
A project does not update module by module. Core, every module and every theme are released together on one version line, a train, named by its MAJOR.MINOR. grim update moves all of them onto the newest train that the core constraint in grim.json allows, then writes grim.lock again, whole.
Given the name of a module or a theme it does the smaller thing: that one unit, to its newest release on the train the project is already locked to. It will not cross into a newer train for one unit, because a module from one train against the core of another is exactly the combination trains exist to rule out.
Usage
$ grim update --dry-run # the plan, nothing changed
$ grim update # the whole project, onto the newest train
$ grim update cart # one module, inside the locked train
$ grim update floatv3 # one theme, the same wayRun it from the project directory, logged in with grim auth:login, in a project that already has a grim.lock. Run the dry run first, every time. It is the only place you see which files are about to be removed from the project before they are.
Arguments and options
Move the project onto the newest release train matching grim.json
Usage
grim update [--dry-run] [-f|--force] [--allow-dirty] [--no-docker] [--] [<unit>]Arguments
| Argument | Description |
|---|---|
unit |
Update a single module or theme, inside the locked train (optional) |
Options
| Option | Description |
|---|---|
--dry-run |
Show the plan without applying it |
-f, --force |
Reinstall units already at their target version |
--allow-dirty |
Run over uncommitted changes to tracked files (instances that track their whole tree) |
--no-docker |
Leave the containers alone: no composer install or migrate after a train move |
What it actually does
In this order, and every step is printed as it happens.
- Checks every package can be had. An archived module, a private one you have no access to, or one that does not exist stops the whole update before anything is fetched, and says which.
- Picks the train. Resolves
corefromgrim.jsonto a grim-core release and takes itsMAJOR.MINOR. That is the train for everything else. - Prints the plan. A headline such as
Train 2.4 → 2.5 available (core 2.5.0), then one row per unit that moves: current version, target, and the constraint that allowed it. With nothing to move it printsEverything is up to date on train 2.5.and stops. - Lands core. The new core release is merged into the working tree against the installed one, as described in When the instance tracks its whole tree. Your
grim.json,.env,storage/, and the rootREADME.mdandCLAUDE.mdare never overwritten by core's copies; of.gitignoreonly grim's block between# >>> grimand# <<< grimis rewritten. Before 1.0.50 core brought its own README and the monorepo's agent guide into every project; an update now replaces those two copies with short files about this project (Wrote the instance's own README.md and CLAUDE.md) and leaves a README orCLAUDE.mdyou wrote yourself alone. One exception:CLAUDE.mdhas to import@AGENTS.md, which loads grim-core's agent map into Claude Code. ACLAUDE.mdof yours without the import gets it as its first line and keeps the rest; one grim wrote earlier and nobody edited is replaced by the current one. The same goes for a README grim wrote before model A and nobody edited: it is replaced by the current one. Not in--dry-run. - Removes what core stopped shipping. Files the previous grim-core release carried and the new one does not are deleted, and each is named. See what gets removed.
- Lands modules and themes. Each moves to its new release, merged with your edits as described in When the instance tracks its whole tree.
- Writes
grim.lock. The train, the core version and commit, and every module and theme with its version and commit.config/modules-available.phpis regenerated from it, and so are the Claude Code skill shims.claude/skills/module-<slug>/SKILL.md— one persrc/<Module>/AGENTS.md, written by core's.claude/sync-module-skills.php(gitignored; an update with nothing to move refreshes them too). - Catches the container up. When core moved:
composer installandphp artisan migrate --forcein the running app container. If the environment is down, or Docker is missing or not running, it says so and leaves that to the nextgrim up.--no-dockerskips this step, for a machine without Docker such as a CI job that runs composer and the migrations itself. - Offers to prune. Modules left on disk that nothing in
grim.jsonneeds any more are listed, with a prompt to remove them now. - Refreshes the JavaScript lock. When the update moved a
package.jsonorpnpm-workspace.yaml, or the project has nopnpm-lock.yaml,pnpm installruns on your machine. See JavaScript dependencies.
A single-unit update skips steps 4, 5 and 8, not step 10: a module or theme may carry a package.json of its own. It never touches core, and the rest of the lock is carried over as it was. A theme is found by its name in either slot, so grim update floatv3 moves the admin theme and nothing else.
Either way the lock lists only the themes grim.json still declares. A theme you took out of a slot leaves the lock on the next update; the directory stays on disk until grim prune removes it.
JavaScript dependencies
pnpm-lock.yaml belongs to the project, not to core. Core brings the root package.json and pnpm-workspace.yaml, a module or theme may bring a package.json of its own, and the lock that pins them together is resolved in the project and committed there. Step 10 runs pnpm install without --frozen-lockfile and ends with ✓ pnpm install — commit pnpm-lock.yaml with the update when the lock changed, or ✓ pnpm install — pnpm-lock.yaml unchanged, nothing to commit when it came out the same.
It runs only when the update moved one of those manifests or the lock is missing. With a conflict in one of them it stops at … in conflict. Resolve the conflicts, then run `pnpm install` and commit pnpm-lock.yaml., and without pnpm on your machine it only asks you to run it. --dry-run runs nothing and says Would run `pnpm install`: the update moves a package.json or pnpm-workspace.yaml …, or, when there is no lock yet, Would run `pnpm install` to write pnpm-lock.yaml, which the instance commits.
A project whose core still carried the lock (grim-core before 1.3.43) sees it handed over on the first update past it, with the target version in the line: Kept pnpm-lock.yaml — grim-core 1.3.44 no longer carries it; it belongs to the instance now. Nothing is deleted. The production image installs from that lock with --frozen-lockfile, so building a workspace without it stops at pnpm-lock.yaml is missing — run `pnpm install` and commit pnpm-lock.yaml.
What gets removed
A file that left core upstream would otherwise stay in every project for good: a deleted test that keeps failing, a deleted class that keeps autoloading. Update removes the difference between the old core release and the new one, with these exceptions.
| Case | What happens |
|---|---|
| Unchanged since the old release | Removed, and named: Removed <path>. The dry run prints Would remove <path>. |
| Edited locally | Kept, with a warning. An edited copy of a file upstream dropped is more likely deliberate than drift. |
| Instance state | grim.json, grim.lock, .env and storage/ are never touched. |
| Files grim manages | The infrastructure files grim upgrade writes, and config/modules-available.php, are left alone. |
| A module that left core | Nothing is deleted. You are told to add it to grim.json or run grim remove. |
When the old core tree cannot be read, for example because the lock records no release version of core, the train still lands and update prints that nothing was removed. It does not guess.
When the instance tracks its whole tree
An instance whose .gitignore is a denylist and whose modules and themes carry no .git of their own tracks core and every unit as plain files. Update then lands the new releases as a diff in the working tree and never commits it.
- It needs a git repository and no uncommitted changes to tracked files.
--allow-dirtyruns it anyway. - The installed release in
grim.lockis the base. A file you have not edited takes the new version, or is removed when the new release dropped it. A file you edited is merged three ways withgit merge-file. - A clash leaves the usual conflict markers. A file you edited that upstream dropped stays (
modify/delete). A file you deleted that upstream changed stays deleted (delete/modify). A new upstream file you already have with other contents isadd/add. A binary or a symlink changed on both sides keeps your copy as a conflict. Every conflict is listed,grim.lockis still written, and update exits non-zero, likegit merge. - The exceptions in the table above still hold, and project code outside core's tree and the unit directories is never read.
- It ends with a commit message to use:
chore(grim): core 1.0.62 → 1.0.63, cart 1.0.62 → 1.0.63.
Release trees are kept in .grim/upstream/, so a tree fetched once is not fetched again. An instance from before this model — an allowlist .gitignore, units with a .git of their own — is refused; run grim adopt first.
Pitfalls
~2.0 is not a pinned train
Composer reads a two-part tilde as >=2.0 <3.0.0, so "core": "~2.0" follows every train of major 2. To stay on one train, write "core": "2.4.*". Either way, no module ever drifts off the train core landed on.
Update does not touch Docker files
docker-compose.yml, docker/ and the Dockerfiles come from the templates of the grim you have installed, not from core. After grim self-update, run grim upgrade to bring them in line. An update that finds a Dockerfile building FROM an image in ghcr.io that is not one of grim's base images says so at the end — grim install --regenerate-docker rewrites them and keeps the old ones as *.bak.
A conflict stops the update half-way
Every conflicted file is listed, grim.lock already names the new versions, and update exits non-zero. Resolve the markers as after a git merge, run the suite, and commit the whole result with the lock. git checkout . undoes the update instead, since the tree was clean when it started; files the new release added stay behind as untracked, and git status lists them.
A single unit refuses to move
Train 2.5 available; run `grim update` (train) means a newer train exists and the project is not on it. Update the whole project first; single-unit updates only work inside the train in grim.lock.
The name is neither a module nor a theme
"x" is neither a module under "requires" nor a theme in a slot of grim.json. — the argument is looked up in requires first, then in the frontend and admin slots. Check the spelling against grim.json; a package the project does not declare is added with grim require, not updated.
A unit has no release on the new train
⚠ module cart has no release on train 2.5 (~2.0) — left as it is. in the plan means everything else moves and that unit stays on its old version. Wait for its release on the train, or loosen its constraint in grim.json if the constraint is what rules it out.
--allow-dirty runs the merge over uncommitted changes, and then nothing separates your edits from the update in git diff. Commit first unless you know why not.
After it finishes
Run the suite, then look at what changed before committing the new lock, grim.lock and, when update said so, pnpm-lock.yaml.
$ grim test
$ git diff --statThe whole move, with the review and the restart around it, is the recipe Move a project to a new train.
Related spells
grim upgrade
Renew the infrastructure files
Rewrite the Docker and nginx files of a project from the templates of the grim you have installed.
grim self-update
Update grim itself
Replace the grim phar with the newest release. Projects are not touched.
grim why
Ask why a module is here
Show the chain of requirements that leads from grim.json to an installed module, and what that module requires in turn.
grim prune
Clear what nothing asks for
Delete the modules under src/ that can no longer be reached from the requires in grim.json.
Recipes that use it
Move a project to a new train
Read the train plan, decide what core stopped shipping, update, renew the infrastructure files, and commit a lock you have tested.
Take one module fix mid-train
Read the one-row plan, move a single module to its newest release on the train the project is locked to, run its migrations, test, commit the module with the lock and land it on the other machines.