From warden
Use when adding a null/range/format check inside an internal helper that already receives a typed argument; deciding where to validate input that crosses a trust boundary (HTTP handler, CLI args, queue message, deserializer); reviewing a diff that scatters defensive guards through the call graph below the entry point; writing parsing/coercion logic that turns external data into a domain type.
How this skill is triggered — by the user, by Claude, or both
Slash command
/warden:et-0003-validating-at-boundariesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
<!-- generated from tenets/ET-0003-validating-at-boundaries.md by `uv run poe build` — do not edit by hand. -->
Type: best-practice · Tier: 1
Validate untrusted input once, at the trust boundary where it enters the system (HTTP handler, CLI parser, queue consumer, deserializer, filesystem read, third-party API response). Inside the boundary, trust your types: do not re-check arguments that the type system already guarantees, do not add null-guards on parameters whose signature forbids null, do not re-validate data that a constructor already accepted. The boundary's job is to turn untrusted bytes into a well-formed domain value; the core's job is to manipulate that value.
Defensive guards scattered through internal code create three durable
problems. First, they duplicate truth: when the rule changes (new
status enum value, new email format), every guard becomes a future bug
site, and the next reader cannot tell which check is the real one.
Second, they hide intent — a if (user is null) halfway down the call
stack tells the reader "this can be null", which silently widens the
contract; later refactors must preserve that nullability or risk the
"impossible" branch firing. Third, they paper over a missing
boundary: every internal guard is a symptom of validation that should
have happened at the edge, where the failure has a meaningful response
(HTTP 400, rejected message, parse error) instead of a generic
ArgumentNullException two layers deep.
// BAD: defensive null-checks on a parameter the type system already constrains.
public Money CalculateTotal(IReadOnlyList<LineItem> items)
{
if (items == null) throw new ArgumentNullException(nameof(items)); // can't be null — non-nullable reference type
if (items.Count == 0) return Money.Zero; // valid, but probably belongs at the boundary
foreach (var item in items)
{
if (item == null) continue; // List<LineItem> can't contain null in C# 8+ NRT
// ...
}
}
# BAD: re-validating an EmailAddress value object inside a domain method.
def send_welcome(email: EmailAddress, name: str) -> None:
if not email or "@" not in str(email): # EmailAddress.__init__ already enforced this
raise ValueError("invalid email")
if name is None or not name.strip(): # `name: str` already excludes None
raise ValueError("invalid name")
mailer.send(email, name)
// BAD: parsing the same shape over and over, never producing a typed value.
function priceOrder(order: unknown): number {
if (typeof order !== "object" || order === null) throw new Error("bad order");
if (!("items" in order)) throw new Error("bad order");
// ...same checks repeated in every consumer of `order`
}
// GOOD: trust the type. Validate once at the boundary (HTTP / deserializer / factory).
public Money CalculateTotal(IReadOnlyList<LineItem> items)
{
return items.Aggregate(Money.Zero, (acc, item) => acc + item.Subtotal);
}
// At the boundary:
[HttpPost("/orders/total")]
public IActionResult Total([FromBody] TotalRequest req)
{
if (!ModelState.IsValid) return BadRequest(ModelState); // boundary validation
return Ok(CalculateTotal(req.Items));
}
# GOOD: validate once in the value object's constructor; trust the type elsewhere.
@dataclass(frozen=True)
class EmailAddress:
value: str
def __post_init__(self) -> None:
if "@" not in self.value:
raise ValueError(f"invalid email: {self.value!r}")
def send_welcome(email: EmailAddress, name: str) -> None:
mailer.send(email, name) # email is guaranteed valid; name is guaranteed str
// GOOD: parse once at the boundary into a typed Order; downstream code consumes Order, not unknown.
const OrderSchema = z.object({ items: z.array(LineItemSchema).min(1) });
type Order = z.infer<typeof OrderSchema>;
app.post("/orders/total", (req, res) => {
const order = OrderSchema.parse(req.body); // boundary: throws → 400
res.json({ total: priceOrder(order) });
});
function priceOrder(order: Order): number {
return order.items.reduce((acc, i) => acc + i.subtotal, 0);
}
dict/
JSON.parse paths, until they reach a typed value object.assert /
guard that throws, not a silent skip.Optional/Maybe), not the call site. The guard buries the contract
violation in a generic exception two layers deep instead of catching
it at the boundary where the response can be meaningful.if — barely any cost."
Compounded across a codebase, defensive guards become the dominant
reading cost. Worse, they normalise the habit: the next contributor
copies the pattern, and the boundary check that should exist never
gets written.NonEmptyList<T>, a PositiveInt value object, a
factory method on the type. Push the invariant into the type
once, then everyone trusts it.npx claudepluginhub c-hoeller/agent-plugins --plugin wardenProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.