Skip to content

/gaia-codify

/gaia-codify looks at what the Code Audit Team keeps finding across merged pull requests, including what the tools its members run report (such as React Doctor (opens in a new tab), axe (opens in a new tab), Knip (opens in a new tab), and pnpm audit), and proposes a guard so the problem stops coming back. For each repeat problem it recommends the lightest fix that would work (the one that adds least to what Claude reads on every task), explains it in plain language, and asks you once to accept the plan. Nothing is authored or switched on without your answer.

It runs like every maintenance command: from the main checkout, on a branch of its own, ending in a pull request. Only you start it, by typing it; Claude never runs it on its own.

Every audit round that catches the same mistake again costs review time and tokens, and the fix lives only in that one pull request. /gaia-codify turns a mistake that keeps coming back into something that stops it before review: a check that fails, a skill Claude loads for that kind of work, or a rule that loads only for the files it covers. The lesson then stays with the project for every teammate and every future session, without making Claude read more on unrelated work, because each rule is scoped to the files it covers and a check adds nothing to Claude’s context. Ask Claude why GAIA prefers a check over a written rule to hear the full reasoning.

The statusline nudges you with 🔨 /gaia-codify (<reason>):

  • Before your first completed review, it counts recurring patterns: /gaia-codify (3 recurring patterns).
  • After a completed review, it stays quiet until something changes, then names it: a new recurring pattern, a pattern that has grown noticeably, or a change in how findings are counted.

Each merge records the pull request’s findings in a comment on it, and the count reads those comments. Only comments from the repository’s owner, collaborators, and organization members count, so a findings comment from anyone else, such as an outside commenter on a public repository, is ignored.

A kind of finding (its finding class) becomes a candidate once it appears on 3 or more different merged pull requests within the last 90 days, at any severity, and no rule from an earlier run already covers it. Findings the audit could not place in a class are counted too, and surface as their own signal when they recur. A finding the audit accepted as a residual instead of fixing (listed in the pull request under ## Accepted residuals (recorded, not fixed), see What gets recorded instead of filed) is left out, because the decision not to fix it is already made. Waived and filed findings still count.

The count reads your merged pull requests through gh. When it cannot, the run says so (“this is not an all-clear”) instead of reporting nothing.

Every finding the Code Audit Team records carries a finding class: a fixed name for the kind of problem it is. Recurrence is counted per class, so two findings about the same kind of problem in different files count as one pattern. The lists below are the classes GAIA ships with. A member picks a class only from these lists, plus the classes your project registers (see Your own finding classes). Claude never invents a class on the fly: a finding that fits none of them is recorded as holistic/unclassified.

A finding a tool reports keeps the tool’s own rule id after a prefix, so any rule the tool has is a valid class.

ClassWhat it is
react-doctor/<rule>A React Doctor rule, such as react-doctor/no-generic-handler-names
axe/<rule>An accessibility rule from axe, such as axe/color-contrast
knip/<issue type>Unused code or dependencies Knip found, such as knip/exports
cve/<advisory id>A security advisory against a dependency, such as cve/GHSA-xxxx-xxxx-xxxx

A tool already catches these, so /gaia-codify recommends making that check stop the change, or adding it to the Quality Gate, never a written rule. Knip is the exception: the Quality Gate does not run it, so /gaia-codify recommends making the Code Audit Team’s frontend reviewer or a CI check fail on that Knip finding instead of only reporting it.

ClassWhat it is
holistic/missing-auth-checkAn entry point skips the authentication or authorization check its peers apply.
holistic/secret-exposureA secret reaches somewhere it must not, such as a client bundle, a log, or source control.
holistic/n-plus-oneA query runs once per item in a list where one query would return them all.
holistic/unnecessary-rerenderA component re-renders because an input changes identity on every render.
holistic/unhandled-promise-rejectionA failed async call reaches no error handler.
holistic/swallowed-errorA failure is caught or ignored, and the code carries on as if it succeeded.
holistic/over-permissive-zodA Zod (opens in a new tab) schema accepts values the code that uses it cannot handle.
holistic/business-logic-in-componentDomain rules live in a component instead of the layer that owns them.
holistic/hardcoded-stringA user-facing string is written inline instead of coming from the translation files.
holistic/non-null-assertionA ! assertion silences TypeScript (opens in a new tab) where the value can in fact be missing.
holistic/hollow-assertionA test or check matches more than it names, so it stays green when the thing it tests breaks.
holistic/uncoupled-restatementA comment or doc describes a named function, route, or setting, and the description is wrong.
holistic/stale-figureA count in a name or comment no longer matches what it counts.
holistic/unarmed-guardA check is correct but is switched on for fewer files than it should protect, so it skips the change it exists for.
holistic/fail-open-discoveryA test or check never loads some of its input, such as a glob that misses .tsx, and still reports clean.
holistic/partial-cause-reportingAn error message names one cause of a failure and stays silent on another cause with the same symptom.
holistic/dangling-referenceA comment or doc names something that no longer exists.
holistic/drifting-duplicateThe same schema, list, or helper exists as separate copies that can drift apart.
holistic/ambient-context-resolutionCode reads its subject from its surroundings (the current folder, a default setting) instead of from the input it was given.
holistic/shared-state-collisionRuns that can overlap write to the same file, cache key, or fixture.
holistic/unbounded-invocationA request or process has no limit, such as no timeout or no page size.
holistic/overclaimed-guaranteeA comment promises more than the code delivers.
holistic/incomplete-enumerationA comment or doc presents a list as complete when it leaves members out.
holistic/repeated-round-tripThe same value is fetched or parsed again for every item or caller, where one call would do.

These match GAIA’s coding conventions for React and TypeScript.

ClassWhat it is
rule/use-effect-derived-stateA useEffect computes state that could be derived during render.
rule/use-effect-state-resetA useEffect resets state when a prop changes.
rule/unnecessary-use-callbackA useCallback the React Compiler makes unnecessary.
rule/missing-effect-cleanupAn Effect that starts something (a listener, timer, or fetch) without cleaning it up.
rule/generic-handler-nameAn event handler named for when it fires (handleClick) instead of what it does (handleClickSave).
rule/switch-statementA switch statement, where GAIA uses if/else chains or object maps.
rule/interface-declarationAn interface, where GAIA uses type.
rule/z-enumA z.enum(), where GAIA uses z.literal([...]).
rule/array-generic-syntaxArray<string>, where GAIA uses string[].
rule/thin-route-violationA route module holding logic that belongs in its page or a service.
ClassWhat it is
workflow/script-injectionUntrusted input, such as a pull request title, reaches a workflow’s shell script.
workflow/unsafe-pull-request-targetA pull_request_target workflow runs code from the pull request with the base repository’s permissions.
workflow/unpinned-actionA third-party action is not pinned to a commit.
workflow/broad-permissionsA workflow or job is granted more permissions than it needs.

A member you add for your own code (see Adding a member) may need classes of its own. Your project registers them in .gaia/finding-classes.json, which ships empty and is yours: /update-gaia never merges or overwrites it. It holds two lists, and both must stay in the file even when one is empty, or the file is read as empty:

  • classes: new kinds of problem, each named <prefix>/<slug> in lowercase words joined by hyphens, such as api/missing-rate-limit. The member’s agent file names them as its own.
  • oracle_prefixes: the prefix of a check tool of your own, such as ruff. Any finding id after that prefix is accepted as it is, the way GAIA accepts axe/<rule>.

/gaia-codify counts a registered class like a built-in one, and file-tech-debt files its findings instead of setting them aside as malformed. Register a class before the member uses it: a class in no list is counted as unclassified. Keep each class a real kind of problem the member can assign the same way every time, not a label for “somewhere in this member’s files”. Ask Claude:

Register the finding classes my <member> audit member reports in .gaia/finding-classes.json, and name them in its agent file.

A finding recorded as holistic/unclassified, or with a class no list registers, is never a candidate, because there is no single kind of problem to write a guard for. When unclassified findings recur on 3 or more pull requests within 90 days, the run shows them as one extra row, marked as not a candidate, and records a suppression on this machine so the signal stays quiet until it rises noticeably. When the same kind of problem keeps landing here, the class list may be missing a class for it, or the cluster needs looking into on its own. /gaia-codify points this out but never adds a class itself. Registering a class later also counts findings that already recorded that exact class. Findings recorded as holistic/unclassified stay unclassified until they age out of the 90-day window.

For each candidate, it first checks whether an existing rule, skill, hook, or Quality Gate step already covers the territory, and recommends editing that before creating anything new. Then it picks the lightest form that fits:

PatternRecommended formWhat /gaia-codify does
An existing tool already catches itMake that check stop the change instead of only warning, or add it to the Quality GateEdits the existing setup
It can be caught by a lint rule, hook, or testAn automated checkWrites a sketch of the hook and script. Nothing is switched on; ask Claude to finish it and turn it on.
The lesson is a procedure (“do these steps in this order”)A skillWrites a SKILL.md outline. Nothing is switched on; ask Claude to finish it.
A judgment call no tool can catchA written rule that loads only for matching pathsWrites the rule file under .claude/rules/, scoped by paths: to where the finding recurs

A written rule is the weakest option: it advises instead of enforcing, and Claude has to read it on every matching task. So before recommending one, it asks what quick evidence would show the rule actually changes behavior. If it can’t name any, it recommends defer or decline instead.

  1. Explain each candidate in plain language: what keeps going wrong, one concrete example (the file and what was wrong there), how often as a share (“on 106 of the last 394 audited pull requests, about 1 in 4”), what the recommendation would actually do, and why.
  2. Show the whole plan as one table.
  3. Ask once:
    • Accept all recommendations (recommended)
    • Change some: pick rows, then choose approve, decline, defer, or redirect (a different form) for each
    • Stop without changes: nothing is authored or recorded
  4. Apply your choice for each candidate. Approved rule edits land in the working tree on the run’s own branch.
  5. Publish approved changes as one pull request at the end, merged only on your answer.
  • Decline records, on this machine only, that you are not acting on the pattern. It comes back only if the pattern keeps growing noticeably: at least 3 more pull requests, and a clearly larger share of audited pull requests than when you declined. It also comes back once after a GAIA update changes how findings are counted, so you can decide again on the new numbers. The record lives in .gaia/local/codify/ and is never shared, so a teammate still sees the nudge.
  • Defer records nothing of its own. The candidate still appears in the next review; the nudge stays quiet until the pattern grows or the counting changes.

A rule /gaia-codify writes carries a one-line comment after its frontmatter saying where it came from:

<!-- gaia-harden: promoted from recurring finding_class <class>; pruned by /gaia-cleanse on obsolescence/redundancy/supersession/duplication only, never for non-recurrence -->

gaia-harden is the command’s old name. Leave the comment in place: it is what stops the pattern from being counted again, and removing it makes the pattern count again. /gaia-cleanse treats a marked rule like any other, except it never removes one because its pattern stopped showing up: a pattern that stops showing up means the rule is working.

The run ends with a cost line.