Essay · July 18, 2026

Craft & engineering

Continuous code hygiene

From agent sessions to curated tooling

Agents see broadly. Deterministic tools do not forget. Between them, the human keeps the judgment — and keeps learning.

The essentials first

Working with AI agents on code quality is a healthy practice — I have done it since the early days of Ezkey. But I eventually had to face an obvious truth: static analysis tools that have existed for years, sometimes decades, still provide value that an agent alone cannot replace. They encode the collective knowledge of expert communities as deterministic, reproducible rules, with no moods and no lapses of memory.

Ezkey's response took the form of a pattern I call curated: for each project stack — React and TypeScript for the Admin UI, Java for the APIs, Kotlin and React Native for mobile — a script runs complementary open-source tools and brings their observations into a filtered, prioritized report. The goal is not to fix everything. It is to reduce the noise until a human can examine the findings that genuinely deserve judgment.

The rest of this article tells the story of how that discipline took shape, piece by piece, and how it eventually extended beyond code into dependencies and runtime security.

Useful but informal one-off sessions

From the beginning of the project, I have run occasional, more or less informal code-quality sessions with different agents. The initial reasoning was simple: doing this work with an agent makes perfect sense. An agent reads quickly, covers a broad surface, and never gets tired. As the models evolved — and as I learned to work with them more effectively — these exercises proved genuinely useful. They steadily improved the quality of the source code.

But a session with an agent remains a conversation. Two sessions do not inspect exactly the same things; something raised one day may go unnoticed the next. That perspective is valuable, but it is not enough to form a discipline.

Proof that deterministic tooling still had a place was already present in the project. In the first few months, I had introduced a formatter and, more importantly, Checkstyle to validate compliance with code-standardization rules on every build. No debate, no mood: the build passes or it does not. For weeks, even months, that mechanism — together with the occasional agent sessions — carried the whole code-hygiene effort.

The trigger: a backend developer facing TypeScript

The push to go further came from a gap, not from a theory. As I have mentioned in other articles, I am a backend developer first. Web technologies are not my original territory; the finer best practices of TypeScript and UI paradigms are not part of my established reflexes.

That gap is precisely why I turned to the open-source linting tools built by active communities of developers who are experts in their fields. They bring a deterministic, rule-driven perspective to code quality. That collective knowledge, that expertise distilled into rules, was exactly what I wanted to bring into the Ezkey discipline — despite, or rather because of, my inexperience with that particular stack.

I approached the work in collaboration with an agent: we researched relevant open-source tools for the Admin UI stack, inventoried them, and evaluated them critically. One early observation from that market review was that these tools are not equal, and they do not all examine the same qualities. Some focus on formatting, some on correct use of stack-specific patterns, and others on accessibility or robustness. If several candidates are retained, they should provide a degree of conceptual orthogonality — three tools should not dig redundantly in one direction while leaving other important conceptual angles untouched.

The curated pattern: reduce noise, preserve signal

That first selection produced a script, and with it the pattern that would repeat. Its job was to run each selected tool with configuration tailored to Ezkey, produce an individual report for each, and then generate a consolidated, filtered report sorted by importance. We called it a curated report: a report designed to surface the most relevant findings in an actionable form.

The problem with all these tools is not that they say too little — it is that they say too much. An initial pass over the Java code produced 440 raw findings. Filtering retained 173, and the review then extracted a first lot of six items that genuinely deserved attention. Those stages matter: the curator does not replace judgment; it makes the volume manageable enough for judgment to operate.

The other half of the pattern is human. A curated pass does not end with an automatic commit. It ends with a finding-by-finding review in which each item receives an explicit decision: fix it, remove it from the report with a durable rationale, or skip it because the signal is not clear enough. Two guardrails emerged through use. First, if a finding cannot be tied clearly to source code, we do not invent an explanation for it. Second, a clear diagnosis does not require a rewrite. When the flagged code is harmless local over-defensiveness, “perfecting” it would require tests and churn for no product gain. If it is not broken, do not fix it. Code hygiene is not a witch hunt.

The signal as an opportunity to learn

High-signal findings eventually took on a second function. They are decision points for potentially actionable items, but also opportunities for continuous learning. Before deciding, I ask the agent to explain what the tool saw: the rule involved, the underlying practice, the established standard from which the code diverges, and the concept's real importance. The discussion is therefore not only about choosing whether to fix, suppress, or skip a finding. It is about understanding what that decision entails.

This step matters even more because, in Ezkey, nearly all of the code is produced by the agent. I still make a few direct edits on occasion, but they are the exception. In that reality, it becomes easy to remain close to the overall intent while drifting away from the precise mechanism that implements it. A report that surfaces a deviation from an established practice then becomes a point of reconnection: before changing the code, I return to the concept, how the implementation expresses it, and why the rule exists.

Code generated by an agent does not stop being source code. It remains a durable, valuable resource that deserves careful maintenance. Delegating its writing does not mean delegating mastery of it or the responsibility to understand it. A curated pass therefore becomes both a hygiene loop and a learning loop: it improves the code while maintaining my ability to reason about the concepts the code embodies.

Prioritizing low priorities

I introduced a principle into this discipline that may sound contradictory: we should not focus exclusively on the highest priorities.

Addressing the most important signals first is obvious. But I like to balance that with a complementary principle I call prioritizing low priorities. Small improvable elements create disorder that we tolerate in the codebase. If we never make room for them, they accumulate until the backlog becomes difficult to recover.

In practice, this means that in each period — days, weeks, sprints, whatever measure makes sense — we identify low-priority, low-risk items: quick wins that can be included with modest effort, in a reasonable dose, after the high-signal items. Doing nothing but urgent work is a subtle form of constant firefighting. Some small signals are canaries: ignored for long enough, they become real problems. They deserve a place, without turning the work into perfectionism that spends too much energy for too little value.

A pattern that generalizes: Java, then mobile

After a few Admin UI passes, the conclusion was unavoidable: none of this was specific to the web stack. The same cycle — tool research, orthogonal selection, scripting, consolidated reporting, triage, and prioritization — could apply to any project component.

For Java, the same critical market evaluation produced a deliberately orthogonal combination: SpotBugs for bugs visible in bytecode, Semgrep with a pinned security-oriented pack, and PMD with a narrow set of design and maintainability rules. Three different angles, one consolidated report. Tellingly, SonarQube — the category reference — was seriously considered and deliberately deferred. SonarQube is a world of its own: a deployable service with APIs, a UI, and a database. It is an important tool that may come later. But for occasional runs that can identify improvement candidates now, with no infrastructure to operate, the lightweight combination won.

The same principle applied to the mobile app, adapted to its terrain: a curator normalizes signals from three complementary sources — Biome for React Native TypeScript, Semgrep for mobile-sensitive patterns such as storage, logging, and cryptography, and Detekt for native Android Kotlin — into one model and one prioritized report. The commit that sealed this step carries a name that captures the intent: unify orthogonal quality signal pipeline.

Three stacks, three levels of personal familiarity — Java, which I know well; TypeScript, which I am learning to tame; and Kotlin, which I know little — and one discipline. That may be the most important point: this pattern works independently of my expertise in a stack because the community rules carry the expertise while the project context guides the judgment.

Dependabot: if it hurts, do it more often

A neighboring practice reinforced this thinking: enabling Dependabot. It is not a static-analysis pipeline, but the posture is the same — reduce a noisy stream into manageable units of decision. I quickly saw that it generated an essentially weekly activity: every project we depend on keeps moving, and update pull requests arrive in waves.

There is a principle in computing that I like to repeat: if it hurts, do it more often. We feel less confident doing activities that we perform rarely. We tend to trust ourselves less with what we do not practice, and when the procedure is informal, each repetition risks a few small omissions. That accumulates uncertainty and risk. The right response is not to postpone the work out of fear of mistakes. It is to do it more often, do it better, master it, and build the tools needed to support it.

At first, I handled Dependabot pull requests one by one. Repeating the check, merge, and validation routine for groups of two, three, or ten PRs was genuinely time-consuming. The method evolved into giving the agent instructions to inventory pending updates, group them into lots by component affinity and risk level, and propose lot-level handling. Three SemVer-compatible patch updates to libraries with a low risk of breakage? Review them as one lot, merge each PR individually, and then run a mini test campaign whose scope is proportionate to the risk. Major or disruptive updates are explicitly deferred, with a durable record of why.

This mechanism supports continuous improvement directly: do not accumulate dependency lag, but balance the effort required by a routine task that keeps returning. The mobile app provided the counterexample last month. An upgrade deferred for too long — React Native, vision packages, and mandatory version alignments between components — took several iterations, including unit tests and tests on a physical phone. That was the clear signal that the practice needed structure. Now that it has one, the task is simply to keep the rhythm. The rhythm is the whole point.

Beyond code: security as executable evidence

More recently, I wondered whether the same principle could apply to securing the application. Ezkey is an MFA platform, so security is central. I began the same process — tool selection, scripting, consolidated and prioritized reporting — for a penetration-testing lane. This is no longer exactly code hygiene, but an extension of the same curation discipline into the running application. This time, the selected tools operate against the runtime. The target is not source text but a complete Docker instance launched through a clean start in a production-like posture: the real code, with its headers, API contracts, and logs, attacked to expose weaknesses grounded in executable evidence.

The comparison that came to mind was this: static-analysis scripts are somewhat like unit tests — they inspect cold code outside execution. Runtime security campaigns are analogous to functional tests that exercise the complete stack after a clean start. They are complementary regimes, as in any testing strategy; neither replaces the other.

Tool selection followed the same logic of orthogonality: an OpenAPI-driven fuzzer that excels at finding error classes generic scanners miss; an established DAST scanner for headers and configuration, passive first; and a template engine used against its usual grain — no broad community CVE packs, but narrow, deterministic custom templates that capture project-specific security invariants. One strict acceptance rule applies: no security conclusion is retained unless deterministic tooling or a documented manual reproduction can reproduce it. Hypotheses remain hypotheses.

The first pass paid off immediately. An administration endpoint returned 500 instead of 401 when the Authorization header was missing — semantically wrong, operationally noisy, and reproduced through direct HTTP replay before being promoted to the backlog with full traceability. It was exactly the kind of signal this lane should produce: small, precise, executable, and fixable.

A deliberate choice: on demand, not a CI gate

Not integrating these tools into a continuous build system is intentional. The project's current posture remains centered largely on validations launched from a development workstation, although a targeted GitHub Action already runs JavaScript and Android validation for mobile. There is no formal release system yet, and the curated tools are not CI gates. They are suited to on-demand evaluation: invocable when needed, with no permanent infrastructure to operate.

This is not a principled position against automation. The formatter and Checkstyle run on every build because compliance with baseline rules should be non-negotiable. It is a matter of fit: each tool belongs at the integration level that matches its function and the project's reality today. When a formal release pipeline exists, the question will be revisited, and some of these scripts will naturally find a place there.

What it all forms together

In retrospect, the thread is coherent: informal agent sessions; a formatter and Checkstyle on every build; a first curator born from an admitted lack of expertise in the web stack; the same pattern generalized to Java and then mobile; a lightweight method to absorb the weekly dependency stream; and an extension of the principle into runtime security.

What matters is not multiplying tools or eliminating every warning. It is building a loop light enough to revisit regularly, strict enough not to invent problems, educational enough to preserve our understanding of the code, and human enough to distinguish a real improvement from a hunt for perfection.

This discipline works just as well on the Java I know deeply as on Kotlin or TypeScript, where I have more to learn.

The expertise lives in community rules; the discipline lives in the method; the judgment remains human.