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.md line in CLAUDE.md, followed as far as it goes — a chain through a third file still counts;
  • a CLAUDE.md that is a symlink to AGENTS.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.