Project Optimizer
Updated October 15, 2026
The project optimizer reads a project the way your agent reads it, and tells you what is missing. It finds the instruction file, checks what is in it, looks at the size of your source files and at your .claude/settings.json, and gives you a score out of 100 with the gaps listed under it.
It is most useful on a project you have just opened. Fix the high findings before you fix any code — an agent that cannot build or test your project will guess, and a guess costs more than the five minutes the fix takes.
The optimizer lives in the right sidebar’s files drawer, under its own optimizer tab, alongside files and read order.
Opening it
- A gem’s ⋮ menu → optimize.
- The gem panel’s AGENT FILES row → optimize.
- The terminal:
crystl optimize --show. - Open the gem’s right sidebar and switch to files (
Cmd + ]cycles the sidebar’s views), then click the optimizer tab.
The first three force the sidebar open on the optimizer tab. Opening the sidebar yourself remembers whichever tab you left it on for that gem, defaulting to files the first time.
A refresh icon beside the tab strip re-analyzes without leaving the tab. Its tooltip reads re-analyze, or already analyzing this project while a scan is running.
The sidebar always shows its own gem. Pick a different gem in the left sidebar and its optimizer tab shows that gem’s analysis. There’s no separate project picker inside the drawer.
When a gem has no project type set yet, the first row of the gap list lets you set one. The type scopes which checks run, so setting it is often the first thing worth doing.
From the terminal
crystl optimize # the project this shard is in
crystl optimize --gem myapp # a named gem
crystl optimize --json # machine-readable
crystl optimize --show # also open the panel for the user
With no --gem, it scores the calling shard’s own gem — not whatever gem is on screen. A worker running in a background gem reports its own project’s gaps. Outside a shard it falls back to the selected gem. If you are in a shard whose gem is not open, it refuses rather than quietly scoring the wrong project.
It is read-only and free on every tier, and it exits 0 even when it finds things. A gap is information, not an error, so it will not break a script that runs it.
--show never moves you. If the gem you addressed is not the one on screen, the panel is refused and nothing opens.
Keeping agents out of the drawer
The drawer itself can’t be turned off; you need it to reach your project’s files. But you can stop agents from popping it open on their own: Settings → General → Workbench, off by default. Turn it on, and an agent-driven open is refused: crystl optimize --show, crystl workbench open, crystl markdown show, crystl sequence open. The refusal tells the agent to ask you to open the drawer yourself, from the pull-tab on the right edge. Your own opens are never affected.
Each finding prints its category and its kind (content/substance: ...), and a finding whose fix is a command prints that command under it as a fix: line. A direction-file finding names no file, because the file is written by the agent the prompt starts, so fix: crystl northstar init is how you act on it.
Two things the CLI does not include: the credential scan (run crystl keys scan) and the mistakes-log pass line only appears when a log is there.
The score
Four category scores, each out of 100, combined by weight:
| Category | Weight | What it is |
|---|---|---|
| content | 3 | what your instructions say |
| files | 2 | which instruction files exist, and how they connect |
| code | 2 | source files that have grown too large |
| setup | 1 | .claude/settings.json, plugin recommendations |
overall = (content×3 + files×2 + code×2 + setup×1) ÷ 8
Grades: high is 75 and up, medium is 40 to 74, low is under 40. The window shows the number beside the word, because high covers a quarter of the range and a word alone cannot tell you that you improved anything.
One number, with the lines beside it
Two scores would be a second grade to interpret, so there is one, and the lines under it carry the rest of the meaning.
agent readiness: 82/100 (high)
structure complete — 3 things only you can fill in
3 checks skipped in this run
a Records gem tops out at 73
The first line splits what is left into structure and substance. Structure is a missing file, a missing section, or two files not wired to each other, and installing the project’s starter kit clears all of it. Substance is what your build command actually is, what your conventions actually are, a placeholder left unfilled, a credential in a public file. No kit can supply those.
The other two lines say how many checks this run skipped, and what this gem’s type can reach. They are two lines because they are two facts: the count belongs to this run, the ceiling belongs to the type. A ruled-out check keeps half its weight, because unknown is not the same as passed, so a Records gem that has done everything asked of it still cannot reach 100. Naming the ceiling is what keeps a skipped check from meaning “waived but charged anyway”.
None of the lines quietens a finding. They count what was reported. Nothing is dropped, hidden, or rescored to make a line read better.
Every finding is labelled file or substance
The label appears with the row: in the window, in --json, and in the CLI’s output as content/substance: .... It is a label and never a filter. Both kinds are reported, both count toward the score, and no check is removed or weakened because a kit cannot clear it. A fresh install scoring less than 100 because the optimizer found real work is the tool doing its job.
Each category is a merge under the hood. Content is the average of the instruction checks and the content-quality checks. Files is the file checks, minus what the AGENTS.md bridge costs, averaged with the cross-file checks. Setup is the settings checks averaged with the plugin recommendations.
Nothing to read scores 50, not 100
If the optimizer cannot find or read a project’s instructions, the content, quality and cross-file checks return 50. If it finds no source files, code returns 60, the floor of the real scan, because an unknown never prices below the worst answer the check could have given.
This is the fix that mattered most. Those checks used to return 100 when there was nothing to read — so an empty directory scored 94 and graded high, while carrying the worst finding the tool can emit. Fifty means “unknown”, which is what it is. An empty directory now scores 59.
A leaked key moves the number
The panel scans the files git would commit for API keys, and those findings now cost points:
| Finding | Cost | Cap |
|---|---|---|
| a file holding a key you saved in crystl | 15 | 45 |
| a file holding something shaped like a vendor key | 5 | 15 |
Both come straight off the overall score, and both tiers stack. On top of that, a project cannot grade high while a key crystl itself holds is in a committable file — the badge is capped at medium. The shaped tier is a guess, so it never affects the badge, and its cap of 15 is chosen so a guess alone can never drop a project out of high.
Those rows are put at the front of the gap list, ahead of every other finding including the other high ones. Full detail on the scan itself, including what “public files” means: Key Scan.
The gem’s type scopes the checks
Point the optimizer at a gem holding finances or notes and, with no type set, it asks for build commands, testing commands, conventions, a verification gate and commit rules. Every one of those absences is correct by design.
A gem type is what stops that. A type that produces something other than software removes the code-shaped checks and adds the ones written for its own kind of project: sources and a citation convention for Research, a drafts and published split for Content, the entry format for Records, a dated log for Personal, and the seven files an orchestrator needs for Orchestration.
The type is stored in NORTH-STAR.md, so it is committed with the repository. Set it from the gap list’s own type row, from gem settings, from the New Gem panel, or with crystl gem type set <type>.
What the type never does is limit what you or an agent may do in that gem.
Where your instructions are
Every content check runs against whatever your agent actually reads. The optimizer looks in this order: CLAUDE.md, claude.md, .claude/CLAUDE.md, then AGENTS.md. It follows a symlink, and it substitutes any @path import in place before it scores a word.
So a project that keeps everything in AGENTS.md and writes @AGENTS.md in its CLAUDE.md is scored on what AGENTS.md says. That is right — it is what your agent reads.
The AGENTS.md bridge
Claude Code opens CLAUDE.md and nothing else. It does not read AGENTS.md. So a project can keep every rule it owns in AGENTS.md, look well documented, and give Claude Code nothing.
The optimizer checks for that. If AGENTS.md exists and nothing connects it to CLAUDE.md, you get a high finding worth 30 points off the files score. Two things count as connected:
- an
@AGENTS.mdline inCLAUDE.md, followed as far as it goes — a chain through a third file still counts; - a
CLAUDE.mdthat is a symlink toAGENTS.md, or a symlink chain that ends there.
The link has to work. A dangling symlink is not a bridge, and gets its own finding.
The same walk names an import loop. If following your @ lines comes back to a file it already read, the finding prints the chain with the repeated file at both ends, so you can see which line to delete.
What it checks
Findings carry a severity and a category. The cost is what the finding takes off its own category score before the weighting above.
files
| Finding | Severity | Cost |
|---|---|---|
| No CLAUDE.md | high | 35 |
| CLAUDE.md is a broken link | high | 35 |
<file> cannot be read |
high | 35 |
| AGENTS.md is not connected to CLAUDE.md | high | 30 |
| No AGENTS.md | medium | 25 |
Imported file <path> does not exist |
medium | 15 each, capped at 30 |
| Instruction imports form a loop | medium | 15 |
| CLAUDE.md and codex.md conflict | medium | 15 |
| CLAUDE.md is gitignored | medium | 10 |
| CLAUDE.local.md not gitignored | medium | 10 |
| .claude/settings.local.json not gitignored | medium | 10 |
“No CLAUDE.md” is skipped when Codex is the active agent and the project has no Claude setup at all — a correct Codex project keeps its instructions in AGENTS.md and is scored on those. “No AGENTS.md” is medium on purpose: nothing is broken, the project just is not portable to a second agent.
The conflict row only fires when a legacy codex.md exists next to CLAUDE.md and the two disagree on naming convention or on a file size limit.
content
| Finding | Severity | Cost |
|---|---|---|
| No Build & Run section | high | 30 |
| No testing commands section | high | 25 |
| Build & Run is a placeholder | high | 20 |
| No code conventions or style guide | medium | 20 |
| No verification command | medium | 15 |
<N> stale paths in CLAUDE.md |
medium | 15 |
| No file size limit stated | low | 15 |
| Build section has no copy-pasteable commands | low | 15 |
CLAUDE.md is very short (<N> words) |
low | 10 |
| Instructions are too vague | low | 10 |
| No known issues or gotchas section | low | 10 |
| No commit guidelines | low | 5 |
| No stated criterion for rejecting work | medium | 6 |
| Nothing your agent reads points at NORTH-STAR.md | medium | 6 |
| NORTH-STAR.md has not moved while this project has | low | 0 |
A few of these need explaining:
- Stale paths names them rather than counting them: three, then
+N more. - Very short fires under 100 words, and prints the count.
- Too vague needs three or more phrases like “write clean code” and fewer than 200 words of content. A long file with one such phrase is fine.
- No verification command is satisfied by a verification section or by a recognised command anywhere in the file —
swift test,npm test,pytest,cargo check,go vet,make test,npm run lint,--noEmit. - No file size limit is satisfied by any phrasing that says a number and “lines” — “600-line ceiling” counts.
- No commit guidelines is satisfied by a commit or git heading, or by a phrase such as “atomic commit” or “one fix per commit”.
- The direction-file rows are the
NORTH-STAR.mdcheck, and at most one of them fires per gem. They are the only checks that ask whether a piece of work should be done rather than whether an agent can do it, and they run on every gem of every type. All three are classified substance, so no starter kit can clear them. The stale row costs nothing: it fires on a project that wrote the file and has been working since, which is the thing the score measures being done.
code
| Finding | Severity | Cost |
|---|---|---|
<N> files over 800 lines |
high | 10 per file, capped at 40 |
One threshold, not two. The finding lists three project-relative paths and a count of the rest. The walk stops after 500 files and says so in the finding when it does, so a count never hides its own denominator. Files whose names say they are not yours — editor backups, conflicted copies — are skipped before they are counted.
setup
| Finding | Severity | Cost |
|---|---|---|
<N> public files contain keys saved in crystl |
high | see the score |
<N> public files may contain keys |
medium | see above |
| .claude/settings.json is not valid JSON | medium | 20 |
| No hooks configured | low | 3 |
| No permission rules in settings | low | 3 |
<Product> project — install plugin |
low | 3 |
<Product> project — re-enable plugin for this gem |
low | 3 |
The two settings rows read the file as JSON, and only fire when the file parses. An empty "hooks": {} counts as no hooks, because that is what it is. Permission rules are looked for under permissions.allow and permissions.deny, where Claude Code keeps them.
A settings.json that will not parse is its own finding. Your agent gets the same nothing crystl does, so every hook and every permission rule in that file is inert — and neither row above can say so, which is why this one exists.
Plugin rows fire when the project shows a signal for a plugin crystl knows about (Vercel, Supabase, Cloudflare, Sentry, Stripe in the seed catalog). They are skipped entirely on SSH gems, because plugin state is on your Mac.
Checks with no score
Some checks report without moving the number.
The mistakes log. If the project keeps one — MISTAKES.md, LESSONS.md, LEARNINGS.md, GUARDRAILS.md, GOTCHAS.md, .claude/MISTAKES.md, or a mistakes section inside the instructions — you get a checked, no issues line saying so, with the entry count. If the log is a file and your instructions never mention it, you get a low finding: your agent only opens that file when told to.
A project with no log gets nothing at all. No finding, no line. Almost nobody keeps one, so a finding here would fire on every project and mean nothing.
The clean key scan. When the scan runs and finds nothing, it says what it covered: checked 7 patterns across 2726 public files. A count with no coverage beside it is a promise the scan cannot keep.
Checks that were removed
If you have seen these before, they are gone on purpose.
- No .claudeignore. Claude Code does not read one. The finding reported a problem that did not exist and offered to fix it with a file that cannot.
- Subpackage missing CLAUDE.md. Claude Code already reads the root file from anywhere in the tree. The row called the normal shape of a monorepo a defect.
- No path-scoped rules. It rewarded a
paths:frontmatter key that an open upstream bug makes inert. Scoring a project up for writing it was telling you something untrue. - CLAUDE.md is too long. Length is not a defect on its own, and the projects that scored worst were the ones that had written the most down.
- No skills, no MCP servers, no rules directory, no settings.json. Six rows that all asserted a problem from an absence. Every project that had not opted into one of crystl’s conveniences got the same list, so the list stopped being about the project.
- Stack not mentioned. It fired because the project had a
package.json, then asked you to write down what that file already says.
The pattern behind all of them: a finding has to be a fact about your project, not an inference from something not being there.
The create north star button
The direction-file finding carries a button, and that is what keeps it from being a nag: the door behind it opens in one click.
It does not write a template. It composes a drafting prompt and hands it to the agent already sitting in that gem, which then reads the repo, drafts what the project looks like it is for, and asks you to correct it. With no file the button says create north star. With a file already there it says revisit north star, and the prompt is seeded with what the file says so the agent asks what changed. crystl northstar init sends the same text from the terminal, and it is the fix: line the CLI prints under that finding.
It is an offer, never automatic. Nothing composes or sends on its own, and the row sits in the list unactioned as long as you like. With no shard open, the prompt goes to your clipboard rather than being lost.
Handing it to your agent
The optimizer tab has an optimization recommendations header just above the findings, with two buttons:
- copy for agent writes the report and puts a prompt that points at it on your clipboard, to paste into any agent you like. The confirmation says where the report was saved.
- launch optimization agent writes the report, then opens a new shard named
optimizein this gem and starts your default agent in it. The agent is told to work through the findings in order, highest priority first, and to ask you before it writes anything only you can decide. It uses the same agent and size as a new shard would: the gem’s own agent if you set one, otherwise the standard command in settings → agents → defaults.
The new shard opens in the gem the optimizer is about. If that is not the gem on screen, crystl does not switch you to it; the confirmation names the gem instead. When no agent is configured, nothing is opened: the prompt goes to your clipboard and the message says what to set.
Both write .crystl/optimization-report.md: the score, all six category numbers, the passed checks, and every gap with its detail. The report is stamped with the time and the crystl version that made it, because the check set moves between releases and an undated report cannot be told from a current one.
The credential rows are deliberately kept out of that file. They are a map of which committable file holds a key on which line, and that file is itself committable. They go in the prompt instead, which nobody commits. When the report withholds rows, it says so rather than printing “no issues found”.
Browsing files
File browsing used to live in its own explore tab. That tab is gone. The project’s file tree is now the files tab itself, sitting beside optimizer rather than under it. Each folder’s + file menu creates a blank file, deposits a saved library file, or scaffolds a new skill. See the files drawer for what that tab shows.
Related docs
- agent instruction files: the files drawer the optimizer tab sits inside
- new gem setup: the pass that runs this analysis on a brand-new gem and acts on it
- gem types: what scopes which checks apply
- starter files: the kits structure findings point at
- NORTH-STAR.md: the direction-file finding, and the button that drafts it