heydeer Sign in
Best practices

Writing effective checks

A good check reads like a rule a new teammate could apply by reading the code. For the file format and options, see Repository checks.

One decidable rule per check

Put each unrelated rule in its own file. Findings are labeled with the check’s file name, and each file has its own patterns.

Write the rule as a condition on the code: what breaks it and what satisfies it.

Scope it with file patterns

Add files patterns so the check applies only where the rule matters; without them, it applies to every pull request. Exclude tests or fixtures with !, and quote every pattern in YAML.

Give the reason and examples

Say why the rule matters, so HeyDeer can judge cases it did not anticipate. Describe correct code as well as what breaks the rule, with a short incorrect and correct example from your codebase.

Keep checks short and ordered

Most good checks fit on a screen. If several checks need the same background, move it into a skill.

Each review applies at most 10 matching checks, in file name order, so prefix the most important ones with numbers, such as 10-auth.md. For a simple check, set skip_extra_reviewers: true; Deep reviews then review it as Standard does. See Up to 10 checks per review.

Test it on a real pull request

  1. Merge the check. HeyDeer reads checks from the base branch.
  2. Open a pull request that breaks the rule and confirm a finding names your check. Confirm that code following the rule gets none.
  3. If the check flags correct code, tighten the rule or the patterns. If it misses a case, add that case as an incorrect example.

Anti-patterns

Anti-patternWhy it failsWrite instead
Vague: “Follow best practices for API design.”There is no condition to hold the code to, so findings vary.“Handlers under src/api/ validate the request body with the route’s schema.”
Invisible to HeyDeer: “Fail unless CI passes and security approved.”HeyDeer does not see CI results or sign-offs.Use GitHub branch rules for required CI and approvals.
Running something: “Run the tests and report the result.”HeyDeer never runs code, tests, or builds.“Changes to src/billing/ include a test that exercises the changed function.”
Formatting: “Use two-space indentation and sort imports.”A formatter or linter enforces this exactly.Run those tools in CI.
Speculation: “Point out anything that might be slow.”Without a concrete pattern, findings become opinions.“A loop must not issue one database query per item; batch the query instead.”
Several rules in one file: “Check auth, logging, naming, and tests.”Findings share one label, and the rules cannot be scoped separately.One file per rule, each with its own patterns.

Before and after

Before:

---
files: "src/**"
---
Follow API best practices. Make sure endpoints are secure, fast,
and well tested, and that naming is consistent. Run the test suite
and fail if anything breaks.

After, as .agents/checks/20-api-workspace-scope.md:

---
files:
  - "src/api/**/*.ts"
  - "!**/*.test.ts"
---
# API handlers scope queries to the caller’s workspace

Every query in an API handler that reads or writes a table with a
`workspace_id` column must filter on the workspace returned by
`requireWorkspace()`. Without that filter, one customer can read
or change another customer’s data.

Fail when a handler queries such a table without that filter.
Pass when every such query filters on the workspace, or when the
handler only touches tables without a `workspace_id` column.

Incorrect:

```ts
const invoice = await db.query.invoices.findFirst({
  where: eq(invoices.id, id),
});
```

Correct:

```ts
const { workspaceId } = await requireWorkspace(req);
const invoice = await db.query.invoices.findFirst({
  where: and(eq(invoices.id, id), eq(invoices.workspaceId, workspaceId)),
});
```

The rewrite keeps one concern, limits it to API handlers, states what fails and what passes, and explains the risk. The other concerns become their own checks or move to tools and AGENTS.md.