Project Optimizer
Updated September 7, 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.
Opening it
- A gem’s ⋮ menu → optimize.
- The gem panel’s AGENT FILES row → optimize.
- The terminal:
crystl optimize.
The window runs a fresh analysis every time it opens. re-analyze rescans without closing it.
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.
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.
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 50.
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 58.
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 sits in a committable file — the badge is capped at medium. The shaped tier is a guess, so it never touches 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.
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 hand 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 sits beside 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 |
A few of these are worth knowing the shape of:
- 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”.
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 lives 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.
Handing it to your agent
Two buttons on the analyze tab:
- send to shard writes the report and types the prompt into the gem’s own shard.
- copy for agent writes the report and puts the prompt on your clipboard, to paste wherever you like.
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 travel in the prompt instead, which nobody commits. When the report withholds rows, it says so rather than printing “no issues found”.
Explore tab
The second tab browses the project’s file tree. Each directory shows its agent instruction files with edit buttons, its subdirectories, and a + for adding a file from your saved library.