Static Checks
Combining AI with static tools like linters or compilers can significantly improve quality.
By introducing static checks, my code reviews are much shorter, and often with no corrections. Static check tools are amazing and an extremely cheap way to give your coding agent feedback on newly written code. And the beauty is that check trigger is deterministic - it does not rely on AI and check itself is deterministic.
Imagine this - I hire a new engineer. I describe our code style in Confluence or in repository docs. What is the chance that this engineer will violate some rules? It is just a matter of time. Same with AI Agent, no matter how hard you try, it will violate rules. Therefore, my strategy is implementing static checks wherever I can.
Quick Start
If you have linter setup and want a quick start, you can use this snippet in your CLAUDE.md:
# Workflow YOU MUST follow
After any code changes YOU MUST:
- [] Run '<build command>' and make sure code compiles
- [] Run '<linter command>' and fix violationsIf you want to learn more, read further.
Build
That is the easiest one as every language has built-in build command. As a responsible engineer, I always compile the code before I push it. Now I ask my agent to do the same, to compile the code before considering task to be done. If the code does not compile, the agent gets instant feedback about issues in the code, so it can take another round and fix errors. In my daily work, builds fail very often and agent takes additional 1-2 turns to make it compile.
- With Go you can use
go build - With Typescript you can use
tsc --noEmit
An easy way to integrate is adding the following lines to your CLAUDE.md:
After any code changes YOU MUST:
- [] Run 'go build ./...' and make sure code compilesLinters and Checks
Linters are tools that perform static analysis of your code and check it against predefined set of rules. Linters and other static checkers are very interesting and often underestimated tools. In the era of coding agents I can not live without them. Too much value to ignore them.
Installing linter on existing project
If you never used linter on your project, it might be painful integrating one on an existing project as it will take some time to clean it up. There’s no shortcut here — you have to fix the warnings first, otherwise your agent will drown in hundreds of them and the integration becomes ineffective. Alternatively, you might want to start slow:
- Start with no rules
- Pick one important rule for you
- Take time to fix this single type of warning across entire project base (Or just use army of haiku subagents to do the job for you :D )
- Go to step 2
Having a clean project with no linter warning makes a huge difference for me, so I would always recommend taking it as a priority.
Go built-in tooling
Go has amazing built-in tools that my agent uses after every code change:
go vet- 35 built-in analyzers. Checks for structured logging, concurrency issues and many more.go fix- technically not a checker, but automatically improves your code, often by taking advantage of new go features. (https://go.dev/blog/gofix)
Even if agent writes something like this, go fix transforms it:
x := f()
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}// After 'go fix'
x := min(max(f(), 0), 100)My favorite linters
I use many lint rules and will only mention a fraction of them here. These are my favorite groups that keep my agents on the rails
Cognitive Complexity and Cyclomatic Complexity
Calculates complexity of the code. How readable and easy to understand your code is. I was surprised how good they are in combination with AI agents. When agent is done coding, it gets instant feedback that the function is too complex to understand, so it gets feedback that the code should be broken down and refactored. I, as an engineer, sometimes produce hard to read code, then I look at it and start refactoring. Same with agent, it might produce hard to read code and cognitive complexity checker gives instant feedback that the code should be improved.
func canBook(r Restaurant) bool {
if r.IsOpen {
if r.AvailableSlots > 0 {
reviews, err := fetchReviews(r)
if err == nil {
if len(reviews) >= 5 {
if r.AverageRating > 4 {
return true
}
} else {
return false
}
} else {
return false
}
} else {
return false
}
} else {
return false
}
}func canBook(r Restaurant) bool {
if !r.IsOpen || r.AvailableSlots == 0 {
return false
}
reviews, err := fetchReviews(r)
if err != nil {
return false
}
if r.AverageRating <= 4 {
return false
}
if len(reviews) < 5 {
return false
}
return true
}Control Flow
Early Return, Error Flow, Superfluous Else, Max Control Nesting - reduces code nesting and spots code that can be simplified which improves readability.
if profile.Exists {
// Business logic here
} else {
return nil
}if !profile.Exists {
return nil
}
// Business logicFunctions/Methods
Function Length, Argument Limit - keeps functions short and clean.
That is only a tip of the iceberg. Find a list of available rules for your language and pick what rules you want to follow. Your agent will comply with those rules.
Custom Checks
Custom linter rules
One underestimated thing is custom lint rules. You are able to write your custom ones. Here is a description of one of them from our repo:
/**
* ESLint rule: prefer-set-over-includes
*
* Warns when using nested .some()/.filter() with .includes() pattern.
* Suggests using Set for O(1) lookups instead of O(n) includes calls.
*
* @example
* // Bad: O(n*m) nested iteration
* items.some(item => selectedIds.includes(item.id));
* items.filter(item => !values.includes(item.value));
*
* // Good: O(n) with Set
* const selectedSet = new Set(selectedIds);
* items.some(item => selectedSet.has(item.id));
*/Surprisingly, it was generated by an agent. You do not need to learn all details about writing linter rules using your language and spend hours or days to write your own rule.
Dependencies Control
I like keeping my project structure neat with explicit dependencies. For that reason, I use dependency checker to make sure I do not violate the rules. Sometimes, I forget to add important details when prompting the task and sometimes my agent breaks the boundaries. For this reason I use depguard linter that does the work for me. In my project, I have the following dependencies:
Search Module → can import from Communities, Reviews and Core
Reviews → can import from Core only
Core → cannot import from anyoneTo enforce one of those rules, I add this to my config file:
core-no-internal-deps:
files:
- "**/modules/core/**"
deny:
- pkg: "github.com/org/app/modules/**"
desc: "core must not depend on any internal module - it is a shared foundation"
allow:
- pkg: "github.com/org/app/modules/core/**"The setup is easy, and it saves me tons of time. My reviews after agentic sessions are much shorter because those rules are never broken, they are enforced, statically.
Integration
When I use multiple tools I always make one command to run them all. e.g. make check or npm run check
# Makefile
check:
go build ./...
go vet ./...
golangci-lint run ./...
go test ./...// package.json
"scripts": {
"check": "tsc --noEmit && eslint ."
}For a long time I had this in my CLAUDE.md:
After any code changes YOU MUST:
...
- [] Run 'make check' and fix errors
...This is a strong start. However, this is not ideal solution as with long conversations agent might start ignoring the workflow. A more reliable way is to use hooks. I cover this in a dedicated article — Hooks. But I would recommend starting with guidelines.
If your project has no linter
If you do not have a linter set up yet, you can delegate the entire process to your agent. Copy the prompt below into Claude Code and adjust steps based on your existing setup.
The goal is to enable Claude Code to use static checks after code changes.
I want you to give me a detailed plan which I can verify before you start.
1. Research the most popular linter for my programming language
and recommend it along with the following rules:
- Cognitive complexity / cyclomatic complexity
- Early return / superfluous else
- Max control nesting depth
- Function length limit
- Argument count limit
2. Install the linter I pick with a minimal rule set
plus the rules I approved from your recommendations.
I don't want hundreds of rules — start clean, I will tighten up with time.
3. Run the linter, make sure it works, then adjust the configuration
so that I do not receive warnings on existing code.
The goal is to make sure the code is not getting worse than it is now.
With time I will be fixing the code and adjusting the configuration.
4. Update my CLAUDE.md — at the top create a Workflow section that says:
"After implementing changes you MUST follow this workflow:
1. Verify the code builds using <build command>
2. Run linter to verify there are no violations"Intelligence of the model matters a lot, but the quality of your codebase and your guardrails matter just as much. I was getting impressive results since Sonnet 3.5. And I believe that this feedback loop was one of the key factors.