This keeps monorepo usage explicit: point
--cwd at the app, while snapshots and budgets are stored
at the detected workspace root.
init
Set up crust in a project: first snapshot, starter budgets, CI configuration.
Seven steps, each reporting what it found and what it decided:
- Detects the Next.js app. The directory
--cwdpoints at wins. In a monorepo with several apps and no pointer, init lists them and stops rather than analyzing the wrong product. - Checks for a production build, naming the build command for the detected package manager if there isn’t one.
- Analyzes it and records the first snapshot, with route count and analysis confidence.
- Reports whether source-map attribution is available, and what is still measured without it.
- Writes
.perf/budgets.json— see starter budgets below. - Generates CI configuration for the detected provider, pinned to the crust version that wrote it.
- States the three rules that need no budgets at all.
--force; init reports
them as kept.
Starter budgets
Generated budgets are derived from one build, so the file carries a"//" array explaining every
number and separating what was measured from what crust chose:
.perf/budgets.json
readBudgets ignores the key. A shell floor appears only when a route in this build emitted a shell
crust could measure; no per-route ceilings are generated, because crust cannot know which routes
deserve a tighter number than the rest.
.perf/ holds generated snapshots, the mutable findings.jsonl trust log, and reviewed project
configuration such as budgets. Snapshots and findings travel on the perf-history branch; the
budget file is a decision worth reviewing in the application branch. init suggests the ignore rule
that separates them—.perf/* with !.perf/budgets.json.analyze
Analyze a production build and record a snapshot.
By default,
analyze compares against the newest local snapshot with the same snapshot schema,
bundler, Next.js major, and at least one matching route identity. Its concise view explains this
build: build health, measurable analysis confidence, actionable regressions, the largest
first-party contributor, shared causes, and trimmed cause chains. --verbose expands the coverage
denominators and unresolved evidence.
Use diff [base] [head] for the decision-first comparison surface. It can compare the current
.next build with one stored baseline or resolve both sides independently from stored snapshots.
diff
Compare two builds. Exits non-zero if either side has no stored snapshot, or if--build could not
produce one.
HEAD~3. base defaults to
HEAD~1.
With one argument the head is the build in .next, and a branch base resolves to its merge base with
HEAD - the head under review is this working copy, so the point the two share is what the change
should be blamed against.
With two arguments nothing is rebuilt and neither side has anything to do with what is checked out:
each ref resolves to the snapshot at that ref, or the newest one its own ancestry can reach. That is
what makes crust diff v1.2.0 release/next answer about those two builds while you sit on a third
branch.
Build both refs first
--build fills the gap two named refs leave: someone has to have measured them. It records each ref
in its own detached git worktree and then runs the comparison above on the two snapshots it wrote.
Quote a command with arguments:
--build 'pnpm --filter web build'. In a monorepo, point --cwd at
the package: crust diff develop feature --build --cwd apps/web.
Your checkout is never touched. The worktrees are detached at each tip, live under the system temp
directory, and are removed whether the build succeeds or fails - so this works from a dirty working
tree on an unrelated branch, which is the situation it exists for. Both snapshots are written to the
project’s single .perf/, exactly where crust analyze on each ref would have put them, so the next
comparison of the same two tips rebuilds nothing.
A tip that already has a comparable snapshot is not rebuilt. “Comparable” is checked as a pair: two
snapshots at the right commits that cannot be compared to each other - a bundler swap, a Next major,
or another package in the same monorepo store - are rebuilt rather than reported as
CANNOT COMPARE THESE BUILDS.
Dependencies come from this checkout: crust links its node_modules into each worktree. When the
ref’s lockfile differs from the installed one it refuses instead, because the bundle that install
produces is not the one that ref describes - build with an install step to measure it:
.env.local above all - are not
in it; export the variables in your shell, or set them in the build command.
A failed build or analysis names the ref it failed on and exits non-zero. It never falls through to a
comparison of one side against an older snapshot.
Output
Output leads with the decision (BLOCK, REVIEW, CLEAR, CANNOT DECIDE), the changes behind it -
regressions and improvements together, each with the strongest source location and the likely next
action - the cause those changes share, and the attribution the comparison rests on. The route table
and per-route detail follow.
diff reads .perf/budgets.json so the decision it prints is the one ci would reach on the same
pair. It does not enforce: the exit code is non-zero only when a side has no stored snapshot.
Material configuration changes that make the builds unsafe to compare appear as comparison warnings
and suppress enforceable route-level conclusions.
ci
Check automatic regressions and configured budgets, then print a PR comment. Exits non-zero on a breach.main. Branch refs resolve to their merge-base with HEAD, not the
latest snapshot at the branch tip.
Without .perf/budgets.json, rendering-mode drops, newly uncached reads, and shell disappearance
still fail when the baseline is comparable. A budget file adds byte, growth, and shell-ratio
ceilings. See regressions and
budgets.
Every ci run records the current build before exiting, including runs that find a breach. If no
baseline exists, crust warns that regressions cannot be detected; absolute ceilings still apply.
Blocking breaches are also appended to .perf/findings.jsonl so agreement can be measured over time
— see findings.
findings
Record and score whether authors agreed that a blocking finding was a real regression.ci writes one row per blocking breach. Each row has a fresh occurrence id, which is what you mark,
and a content key for identical breaches. Mode and cache failures retain that key across runs; byte,
growth, and shell checks can receive a new key when the measured values in their message change.
rate reports disputed / (agreed + disputed) and leaves the rate unmeasured until something has
been marked—an empty log is not a 0% disagreement rate.
Target before ownership routing ships: fewer than 1 in 10 resolved findings disputed.
report
Write a self-contained HTML report.analyze prints, worst first, each with its evidence and a concrete next step —
then shared-cause blast radius, then the route table. A jump bar names the sections the report
actually has; its Search routes control focuses the table’s search box.
Findings and shared causes each show the three worst and fold the rest behind a disclosure that says
how many there are. The fold is a native <details>, so browser find-in-page opens it to reveal a
match.
The report also supports:
- search across route patterns, components, and source files
- filters for dynamic, partially static, heavy, and unattributed routes
- grouping by layout or workspace package
- full cause and import chains with evidence labels
- shared-cause blast radius that filters the affected routes
- PR-ready shared-cause explanations
--out is resolved from the shell’s current directory, not from --cwd. In a monorepo, either run
the command from the app directory or pass an explicit path such as
--out apps/web/crust-report.html.
mcp
Serve this project’s stored snapshots to an MCP-capable agent over stdio.list_builds, build_summary, route_detail, explain_route_cause,
compare_builds, cause_blast_radius, route_history, build_findings. See
Ask an agent for what each answers and the guarantees they hold to.
The tool names, inputs and response shapes are not covered by the CLI stability rules yet — see
COMPATIBILITY.md.
ask
Run one of those tools here and print its answer. No agent, no client, no registration.key=value rather than flags, so a tool can gain an argument without colliding with
--cwd or a future crust option. Output is the same JSON the agent receives. A refusal exits 1.
Use it to check an answer the agent gave you, or to see the evidence without wiring anything up.
manifest
Write the snapshot where the in-app panel can fetch it.history
Sync snapshots with the orphanperf-history branch.
push reports and exits cleanly when the remote rejects it - a fork PR’s token is read-only on the
base repository, and that is not a failure worth breaking the check over.
prune
Apply the retention ladder.synthetic
Measure routes against a running deployment. Needs Playwright as an optional peer.list
List stored snapshots, newest first.Exit codes
crust ci exits with 1 when an automatic regression or configured ceiling is breached.
crust diff exits with 1 when it cannot find the requested baseline, and with --build when a
build or an analysis of either ref failed; a displayed regression does not make diff a CI gate.
Other successful commands exit with 0.
Any unsupported project - Next outside 15–16, a dev-only build, a Pages Router app - throws with a
message naming the reason rather than emitting numbers it cannot stand behind.