Coding agents

Coding agents leave the same marks on every codebase they touch: a second copy of code that already exists, arithmetic that does nothing, and tests that cannot fail. Ur’s check can run at the end of each of an agent’s turns and send it back to work until the check is clean.

Setting a project up

From the project’s root:

ur init

ur init adds ur check --hook <agent> --fix as the stop hook in the project’s settings for each coding agent with stop hooks, creating the settings file where there is none and leaving a hook that already runs ur check alone:

Agent Settings Event
Claude Code .claude/settings.json Stop
Codex .codex/hooks.json Stop
Gemini CLI .gemini/settings.json AfterAgent
Factory Droid .factory/hooks.json Stop
GitHub Copilot’s CLI .github/hooks/ur.json agentStop

Cursor runs Claude Code’s hooks, so Claude Code’s settings cover it too. ur init writes no ur.toml: a project without one is configured by its ecosystem’s files, as its current tools are. Commit the settings files, so every agent working on the project runs the check.

What the hook does

When the agent ends its turn, the hook runs ur check --fix over the project: it writes the fixes of the findings that have one and checks the files changed since the last clean check. A clean check lets the agent stop. Otherwise the hook blocks the stop and hands the agent Ur’s report as the reason, so the agent reads the findings and fixes them.

An agent that cannot fix what the check finds would never stop, so when the agent is already continuing because of the hook, the same findings a second time let it stop.

The rules’ instructions

Each rule gives an instruction for an agent to follow while it writes code, so the agent avoids the mistake rather than fixing it after the check. ur lint --rules prints them, and they can go in a project’s AGENTS.md or CLAUDE.md.

These are the instructions of the rules on by default; an optional rule's page gives its own.

- Before writing out a set of literal values or a particular constant, look for a definition that already holds them and refer to it instead. (`duplicated-data`)
- Never add to or remove from a collection inside a loop that walks it; walk a copy or build a new collection. (`mutated-while-iterating`)
- Write `x not in xs` and `x is not y`, never `not x in xs` or `not x is y`. (`negated-operator`)
- Before writing a function, look for one that already computes the same result and call it instead of writing another. (`semantic-clone`)
- Read and write environment variables only in the project's env file, such as src/env.rs or env.py, and pass the values on from there. (`stray-env-var`)
- Write no operation or conditional whose result is a constant or one of its own operands, such as `x * 1` or `a if c else a`. (`trivial-expression`)
- State the value a test expects as a literal rather than computing it, so the test cannot share the code's mistake. (`circular-expectation`)
- Make every check in a test run on every path, assert conditions rather than panicking under an if, and compare whole collections with one assertion rather than element by element in a loop. (`logic-in-test`)
- Give every test an assertion on the result of the code it calls; a test that only calls code can fail only by crashing. (`missing-assertion`)
- Write the values a test checks in the test itself, so it can be verified by reading it. (`opaque-test`)
- End a test after checking the scenario it is about; put an independent scenario and its checks in a test of its own. (`overloaded-test`)
- Give each parameter of a call under test a different value, so a test fails when the code swaps or confuses them. (`repeated-argument`)
- Compare the result of the code under test with a value stated independently of it, never with the same expression. (`tautological-assertion`)
- Pass the code under test values other than its types' defaults, such as 7 or "apple" rather than 0 or "", so a test fails when the code ignores its input. (`trivial-inputs`)
- Pass the two values to an assertion that compares them, such as assert_eq! or assertEquals, rather than asserting that a comparison holds. (`uninformative-assertion`)
- Make every assertion able to fail: compare the result of the code under test with a value stated separately, and call the matcher or assertion method rather than only naming it. (`vacuous-assertion`)
- Before citing a lemma in a Lean proof, check that the lemma is proved, and prove it first if it is not. (`rests-on-unproved`)
- Prove every Lean theorem: leave no `sorry` or `admit`, declare no `axiom`, and prove decidable goals with `decide` rather than `native_decide`. (`unchecked-proof`)
- Give each construct condition tags that, with those around it, hold for some release, dialect, or set of features. (`grammar-dead-condition`)
- Tag a construct with a condition only where it narrows what the constructs around it already allow. (`grammar-redundant-condition`)
- In an ordered choice, put no alternative after an earlier one that matches wherever it would. (`grammar-shadowed-alternative`)
- Define a sort only when a start rule reaches it, directly or through other sorts. (`grammar-unreachable-sort`)