Skip to content

How it works

Last updated: 2026-09-07

Three moments

The checks run at three moments.

Moment What runs How long
Every time a file is saved by an AI coding agent the scanner on that one file under a second
Every commit the scanner on the lines you added, and a check that the commit message is sensible seconds
Every push the battery, on what the push changed: build, tests, linter, formatter, the scanner on everything since your last push, duplicate-code detection, and a check that your main branch is protected seconds for a push of documents; as long as the affected modules' build and tests for a push of code

A refusal at any moment stays on your machine.

A push runs what it changed

The push hook reads what the push changed before it runs anything, and answers with one of three words:

  • none — only documents, plans or other prose changed. No build, no tests, no linter, no formatter, no duplicate-code run: none of them could have an opinion. The scanner still reads the added lines, and the protected-main check still runs.
  • files — code changed. The linter and the formatter read the changed files. On a Swift package the build rebuilds the targets that changed and every target that depends on them, and the tests run for the test targets that depend on them — the package's own graph decides, so a change in a leaf module never runs the whole suite, and a change in the module everything imports runs everything. On Android the Gradle modules play the same part (each affected module builds and runs its own tests); on the web the npm workspaces do, and when the test script is jest or vitest the runner is handed the changed files and runs the tests that import them; in Python every file's imports are the graph, so a push runs exactly the test files whose imports reach what changed. A file no module owns runs the whole battery. An Xcode project has no graph the hook reads, so its build and tests run whole when code changed; its linter and formatter are still file-scoped. Type checkers (tsc, mypy) always read the whole program, because that is what a type check is.
  • all — the checks themselves changed (a check, a linter configuration, a baseline, the package manifest), or the environment asked for everything (COAST_SCOPE=all, which CI sets). Everything runs.

The starting lines follow suit. A repository adopted with warnings or findings carries a count per file beside its total (written by adopt.py --measure-tools), so a scoped push is judged on the files it rebuilt or linted: their count may not rise, and the files it did not touch keep their numbers. A starting line without the per-file counts runs that check whole, and says which re-measure enables the scoped run.

The scanner

Most of the rules are about a literal in the wrong place: a user-facing sentence typed straight into a screen instead of a strings file, a colour or a font size typed into a view instead of the theme, a password in code, a plain http:// address, a sleep on the main thread. The scanner has a list of these patterns for each platform and looks for them in the lines you added. It knows what kind of file it is looking at (a screen, a test, the theme, a strings catalog) and applies only the patterns that make sense there.

It does not read your program's structure.

Your platform's linter

Where the platform already has a good tool (SwiftLint, detekt, ESLint, ruff), the installer ships a configuration with the rules the standards cite switched on, and the push runs it. You can edit that configuration; the installer will not overwrite your edits.

Duplicate code

jscpd finds copied blocks of code across your project. The rule is "no new duplication": the copies you already had are recorded at install, and a push is refused only when it adds a new one.

Three kinds of rule

Every rule is labelled with what holds it, and the label puts it in a bin:

  • Enforced by a check. A check refuses the change. This is the number you see.
  • Advisory. The scanner mentions it. Nothing fails.
  • Needs a reviewer. Only a person (or a review agent reading the code) can judge it: naming, whether an abstraction is premature, whether an error message is honest. These are listed in your rules file as review-only.

The number "rules enforced by a check: 24 of 74" is computed from your project's own copy of the rules and written into your CLAUDE.md. A rule counts only if the check it names exists and runs.

Starting lines and deadlines

An existing project's warnings, style findings, duplicated blocks, and a few scanner counts are recorded at install with a date 90 days out. Each count may fall or stay flat and never rise. After the date, the check requires zero. Only a person can move a date, and the move is written down with who and why.

Guard-rails for AI agents

If you use Claude Code on the project, a small set of hooks stops an agent from doing things it should never do on its own: editing the files that define the checks, running commands that change your cloud infrastructure, skipping the hooks, force-pushing, and ending its turn with work it has not pushed. See Working with AI coding agents.

Where the checks live

In your project. The installer copies the checks, the hooks, and the rules into it, and records the release in .coast/standards-version. Nothing outside the project carries the rules, and nothing needs to be cloned or kept in step on your machine. A newer release is taken by running the installer with --release.

What it never does

  • It never sends your code anywhere. Everything runs locally.
  • It never calls a model. The checks are scripts.
  • It never edits your code. It only refuses.