robustness-at-boundaries — make the edges safe, keep the core simple
Purpose: install the robustness an AI agent reliably forgets. Agents pour complexity into internal structure while leaving the boundaries — where untrusted data enters and where things actually fail — unguarded. Stanford measured the result: AI users wrote more security bugs and were more confident they hadn't. This skill makes the edges trustworthy without turning the core into defensive sludge.
Pair with
craft:right-sized-design: that one stops complexity in the structure; this one adds the safety at the seams. They are the two halves of "production-ready."
Reflex card (the heuristics)
- Validate where data enters, once. At every trust boundary — API edge, request body, query param, parsed file/JSON, env var, external API response, DB row mapped to a type — validate and parse into a known shape. Inside that boundary, trust the type. Don't re-validate everywhere (that's defensive sludge); don't validate nowhere (that's the AI default).
- Never swallow errors. No empty
catch {}, no bareexcept: pass, no_ = err, no error buried in a log line and execution continuing as if nothing happened. Either handle it meaningfully or let it propagate with context added. - Define errors out of existence (Ousterhout). The best exception is the one that
can't occur. Design APIs so edge cases are normal cases: return an empty list instead
of null, make
removeon a missing key a no-op, use a default. Reduce the number of places that must handle an error rather than scattering try/catch. - Fail fast and loud at startup; degrade gracefully at runtime. Missing config →
crash on boot with a clear message, not a
NullPointerExceptionan hour into production. A flaky downstream call → timeout, retry-with-backoff, or fall back — but log it, don't pretend it succeeded. - Error messages are for humans. Include what failed, the offending value (not
secrets), and ideally what to do. Preserve the cause chain (
raise ... from e,%w,{ cause }). A bare "Error" or a raw stack to the user is a non-answer. - Secrets and env-specific values never live in source. No inlined API keys, tokens, passwords, or hostnames. Config comes from the environment (12-Factor). Validate that required secrets exist at boundary/startup.
- Don't return
null/Noneto signal failure where it will silently propagate. Throw, or return an explicitResult/Option. Silent nulls becomeundefined is not a functionthree files away.
Red flags
try { ... } catch (e) {}/except Exception: pass/if err != nil {}(ignored).- A handler that maps a request body straight into business logic with no schema check.
as SomeType/# type: ignore/interface{}used to skip a real runtime check.- A literal secret, key, password, or
https://prod-host...in source. catchthat logs and then continues down the happy path.- Returning
null/-1/""to mean "it failed." - The same null-guard repeated in twenty callers (you should have defined the error out of existence upstream instead).
- A validation library imported but the dangerous field still untyped/unchecked.
The procedure
- Map the boundaries of the change: where does untrusted data enter? where can a call fail?
- At each entry boundary: parse-and-validate into a known type (zod/pydantic/ explicit guard). Reject early with a precise error.
- At each failure point: decide handle-here vs. propagate-with-context. Never silence. Prefer designing the failure away.
- For config/secrets: pull from env, validate presence at startup, fail fast.
- Keep the interior clean: once past the boundary, code trusts its types — no
defensive checks scattered through pure logic (that's
craft:right-sized-designterritory — robustness at the edge, simplicity in the core).
Anti-over-correction
Robustness ≠ defensive paranoia. Don't:
- Re-validate the same value at every layer (validate once at the boundary).
- Wrap every line in try/catch (handle at the level that can actually do something).
- Catch-and-rethrow with no added context (pointless noise). Validation belongs at the boundary; the core stays simple and trusts its types.
Critique mode
For each boundary in the diff: is input validated? are errors handled or meaningfully
propagated? any swallowed error, silent null, or hardcoded secret? Tag findings
[robustness-at-boundaries · swallowed-error|missing-validation|hardcoded-secret · SEV].
Swallowed errors and missing validation on untrusted input are HIGH by default.
References
- Worked before/after:
examples/swallowed-vs-handled.md - Define errors out of existence:
../../references/PRINCIPLES.md§B - Language specifics:
../../references/lang/{python,typescript,go}.md - Rules 6 & 12:
../../RULES.md