DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

When an Agent Removes Input Validation and Trusts Callers

Quick answer: Treat a deleted validation check as a finding until you prove the boundary still rejects bad input. An agent that removes a guard because "callers already validate" is making an assumption about code it does not control, and most real systems have more than one entry point. Verify the callers, then either restore the check or move it to the trust boundary where outside data first enters.

Why agents delete validation in the first place

A coding agent optimizes for the diff it is asked to produce. If you ask it to simplify a function, remove duplication, or flatten a call chain, it looks for lines that look redundant. A guard such as if (!email.includes("@")) throw new Error("bad email") can read as dead weight when the agent traces one happy path and sees the same check a few frames up the stack.

The reasoning is usually a clean-sounding version of "validate once, at the edge." That principle is sound. The problem is the agent's map of the edge. It resolves the callers it can see, marks the shared check as duplicated, and deletes the inner one. It cannot guarantee it found every caller; framework routes, message handlers, and deserialized payloads can reach the function without appearing in a static import graph.

Why "trust the callers" quietly breaks

Input validation is a boundary control, not a courtesy between functions. The moment data crosses from outside the trust zone, a request body, a queue message, a webhook, a stored record, something has to enforce shape and range. Removing an inner check only stays safe if two conditions hold forever: every caller sits inside the trust boundary, and every one of them validates before calling.

Both conditions erode on their own. A helper that starts as internal gets reused by a new route next quarter, or a queue consumer is added that reads the same record type. A caller that validates today gets refactored and loses its check. The failure shows up later as a malformed record in the database, a NaN propagated into a price, or a crash in a path that assumed a value was present.

How to spot the deletion in the diff

You are looking for removed lines that enforced a constraint, not just moved lines. Concrete signals include a deleted throw, a dropped if that returned early on null or out-of-range input, a removed regex or schema parse, and a shorter function whose new version assumes the argument is already well-formed. Pair the removed line with the parser's view of definitions, callers, and imports so you can see who actually reaches it.

A code review built on local Git differences plus parsers that resolve callers and imports will surface the change and let you check the incoming references next to the deleted lines. If the file has no resolved incoming reference, do not read that as permission to delete its checks; an unresolved caller can mean the parser could not see the entry point, not that no caller exists.

What is safe to remove versus what to keep

Situation Call Reason
Same value is checked at the trust boundary and the function is now private, non-exported Can remove No outside path reaches it; one guard at the edge is enough
Function is exported, public, or part of a shared package Keep You cannot enumerate current and future callers
Data comes from a request, webhook, queue, or parsed storage Keep at the boundary Input is outside the trust zone until checked
Check duplicates a type system guarantee (compile-time only) Can remove The compiler already enforces it before runtime
Check guards a runtime value the type system cannot see (parsed JSON, env vars) Keep Types do not run on data that arrives untyped
Agent cannot resolve all callers Keep and investigate An incomplete caller map is not evidence of safety

The fix that does not just undo the work

Restoring the deleted line is the safe default, but the better fix matches where trust actually changes. If the function is genuinely internal, move the check to the single public entry point and keep the inner code lean, so validation happens once at the real edge. If several entry points exist, keep the guard close to the function, since that is the only check every caller is forced to pass. Then send a malformed request and confirm the system rejects it; a static read shows the guard exists, while a real request shows it fires.

Honest limits of the review

The review can miss dynamic loading, framework entry points, and external consumers, which is exactly where a deleted guard hurts most. Identical text in two checks does not prove they are interchangeable, since they may run at different boundaries. The inline review returns priority, evidence, and a recommendation, but it can raise false suspicions or miss a defect, does not prove correctness, and does not run the app. Treat its output as a lead to verify, and use a real request or run check as the proof; missing scripts are reported as unverified rather than passed.

Powerduck puts the flagged deletion next to the diff with the caller and import evidence, and can export a handoff package that lets a coding agent restore the check with the relevant context attached. For more on this workflow, see the guide to reviewing AI coding agents.

Frequently asked questions

Is it ever correct to remove an inner validation? Yes, when the function is fully private and one check at the trust boundary covers every path to it. The burden is proving no outside caller exists, not assuming it.

How do I confirm callers actually validate? Read each caller's check rather than trusting a name, then send a malformed request through the public entry point. If the boundary rejects it, the inner check is redundant; if bad data gets through, restore it.

Does a type system make runtime validation unnecessary? Not for data that arrives at runtime, such as parsed JSON, webhook payloads, or environment variables. Types describe code you compile, not values you receive.

What if the agent says the check was duplicated? Duplication is a reason to consolidate at the edge, not to delete without a replacement. Confirm one guard still runs before any outside data reaches the function.

The bottom line

"Trust the callers" is only safe when you can name every caller and every one of them sits inside the trust boundary. When you cannot, the deleted validation is a liability, not a simplification. Keep the guard at the boundary where outside data first enters, prove it with a real malformed request, and merge the agent's change only after that check fails the way it should.

Top comments (0)