ai-code-blindspots
AI writes the code the prompt literally asked for and stops. The controls a senior adds from unstated context — authorization, tenant scope, validation, transactions, encryption, rate limiting, output encoding, error handling, edge cases, package existence — are invisible to a single-file prompt, so they are the first thing dropped. They are also the classes pattern-linters miss. This skill is the pre-ship checklist: for the surface you just wrote, assert the matching control is present, run the backstop grep, and route to the deep skill when it needs more than a check.
This is a routing + verification layer — it does not re-teach what the deep skills own. It is the "did I supply the invisible controls?" gate that senior-engineering-discipline points at.
When to use
- You have written (or are about to finish) an endpoint, query, migration, UI render, file/network op, infra resource, dependency change, or test — before you call it done.
- Reviewing an AI-authored diff and asking "what did it silently omit?".
- The
senior-engineering-discipline rule fired and routed you here.
Do NOT use when: the change is prose/docs/config-only, or a one-line rename — there is no code surface to check.
The Iron Law
FOR EVERY SURFACE YOU TOUCHED, NAME THE INVISIBLE CONTROL AND CONFIRM IT IS PRESENT.
"NOT MENTIONED IN THE PROMPT" IS EXACTLY WHY IT IS MISSING — CHECK IT ANYWAY.
A CONTROL DEFINED BUT NOT WIRED IS ABSENT. A grep IS CHEAPER THAN A BREACH.
Procedure
- Inspect the change and list the surfaces it touched (endpoint, query, migration, render, file/fetch, infra, dependency, test) — analyze what you actually wrote before checking it.
- For each surface, take its row below: assert every listed invisible control is present (with file:line), and run the matching backstop grep.
- For anything a grep flags or a check can't confirm, open the routed deep skill.
- Emit the per-surface confirmation (Output format); surface any unresolved gap to the user — never silently omit.
Surface → invisible controls → backstop
Run the row(s) for what you touched. The grep is a fast authoring-time backstop (not a substitute for reading the code); zero results is the pass, a hit is a prompt to look.
| Surface you wrote |
Invisible controls to assert |
Deep skill |
| HTTP endpoint / route |
authorization (this principal may act on this resource — not just authenticated); tenant scope; the three negative tests (unauth→401, non-owner→403/404, cross-tenant→403/404); input validation at the boundary; rate limit actually wired; state-changing → CSRF + audit log |
broken-access-control, authz-review, threat-modeling |
| DB query / ORM |
parameterized, never string-built; tenant predicate present; no SELECT * across a serialization boundary; N+1 avoided (eager load) |
source-discovery, security |
| Migration |
reversible (down); expand-contract for drop/rename (never a bare DROP COLUMN before code stops reading it); transaction; index on new FK/filter column |
engineering-safety-floor, migration-architect |
| User-controlled render (FE) |
output-encoded; no dangerouslySetInnerHTML/v-html/innerHTML/eval on non-constant input; no secret/token in client code; token not in localStorage. This surface also carries the four completeness rows below — they are part of it, not an optional extra. |
frontend-render-security |
| Third-party asset in shipped markup / CSS |
delivery is self-hosted through the project's own route (framework font/asset primitive, bundled package, or locally-served file) — a font/icon/stylesheet CDN link transmits the visitor's IP to that third party on every page view; a hotlink only on a stated consumer opt-in; integrity + crossorigin if a CDN link stays |
design-fidelity-mechanics § Asset & imagery discipline (owner), typography-system |
| File / outbound fetch |
path confined to an allowed base; SSRF allow-list + private-IP block on user-supplied URLs; size limit; explicit timeout (no unbounded/default-infinite wait); server-side validation (never client-only) |
security, defense-in-depth |
| Infra / IaC |
least-privilege (no Action:*/Resource:*); encryption at rest; no 0.0.0.0/0 to mgmt/DB ports; no hardcoded creds; scanner-verified, not plan-verified |
engineering-safety-floor, terraform, secrets-management |
| New dependency |
package exists on the real registry (not hallucinated); not typo-adjacent to a popular one; pinned + lockfile committed; CVE-scanned |
supply-chain-intake, dependency-upgrade |
| Secrets/credentials |
never a literal in source; env / secret-manager reference; password columns use bcrypt/argon2, never MD5/SHA; agent/MCP config too — no raw key in .mcp.json / agent-config / CI YAML / a committed .env (env-var indirection, gitignored) |
secrets-management, domain-safety-pii |
| Error path / timeout / retry |
failure handled, never silently swallowed (no empty catch {}); external call has a bounded timeout; retry is capped + backed-off + idempotent (no retry storm); the error surfaced to the caller carries no secret / stack / PII; a partial failure leaves consistent state (no half-written record) |
systematic-debugging, defense-in-depth |
| Concurrency / shared state |
check-then-act guarded (DB lock / atomic op / transaction — not a read-modify-write race); retried write carries an idempotency key; no unbounded parallel fan-out; shared mutable state synchronized |
source-discovery, security |
| Test |
asserts general behavior; expected derived from inputs/seeded data, not hardcoded; boundary + error + abuse cases, not only happy path |
testing-anti-patterns, test-driven-development |
Render surface — the completeness rows
Every row above is a security control. These four are the completeness
controls for the same surface — the states an agent most reliably omits, because
the prompt named the happy path and nothing else. A render that ships only the
populated state is not finished; it is one of four states written.
The grep polarity is INVERTED here, and reading it the other way is the failure
mode this table has. Above, zero hits is the pass. Below, zero hits next to a
render that fetches or iterates is the prompt to look — the state is probably
absent. A hit is not a pass either: the column is a heuristic, a locator for
code to read, never proof the state behaves. A framework abstraction can
implement all four correctly and match none of these patterns, and a codebase can
match all four while shipping a broken empty state.
| State |
Assert on the render you wrote |
Backstop grep (heuristic — zero hits is the prompt) |
| Empty |
the zero-rows / no-results case renders a deliberate state, not a bare frame or a collapsed layout; it says what is missing and what the user can do next |
rg -n -e '\.length\s*[=<]' -e '\bisEmpty\b' -e 'count\(\)\s*[=<]' -e '\bempty\b' — over the file that maps the collection |
| Loading |
the pending case renders (skeleton, spinner, disabled control); no layout shift on arrival; the state is reachable from the actual async call, not only defined |
rg -n -e 'isLoading' -e 'isPending' -e 'isFetching' -e 'Skeleton' -e 'Spinner' -e 'aria-busy' |
| Error |
the failed case renders a recoverable state with a retry or a next step; the message carries no stack, secret or PII (the frontend-render-security row owns that half) |
rg -n -e 'isError' -e 'onError' -e 'ErrorBoundary' -e '\.catch\(' -e 'role="alert"' |
| Keyboard path |
every interactive element is reachable and operable by keyboard alone, with a visible focus ring; a click handler on a non-button element carries a role, tabindex and a key handler |
rg -n -e 'onClick' -e '@click' -e 'v-on:click' lists the handlers, then rg -n -e 'tabIndex' -e 'tabindex' -e 'onKeyDown' -e '@keydown' -e 'role=' on the same file — a handler with no match on the second is the prompt |
Route to accessibility-auditor for the keyboard row when the surface is more
than a handful of controls — this row asserts the path exists, it is not a WCAG
pass.
Backstop greps (authoring-time, cross-stack)
# Frontend insecure render / client secrets (React + Vue + vanilla)
rg -n 'dangerouslySetInnerHTML|v-html|\.innerHTML\s*=|\beval\(|new Function\('
rg -n 'NEXT_PUBLIC_.*(SECRET|KEY|TOKEN)|VITE_.*(SECRET|KEY)|localStorage\.setItem\([^)]*[Tt]oken'
# String-built SQL (concatenation / interpolation into a query)
rg -n 'query\(.*(\+|\$\{|`).*\)|(SELECT|INSERT|UPDATE|DELETE).*(\+|\$\{)'
# Hardcoded secrets / weak hashing
rg -n 'AKIA[0-9A-Z]{16}|sk_live_|AIza[0-9A-Za-z_\-]{35}|password\s*=\s*["\x27]|md5\(|sha1\(' .
# Infra: wildcard IAM / open ingress / disabled TLS
rg -n '"Action"\s*:\s*"\*"|"Resource"\s*:\s*"\*"|0\.0\.0\.0/0|InsecureSkipVerify|verify=false|curl.*\| *bash' .
# Raw secret in an agent/MCP/CI config (should be an env-var reference, not a literal)
rg -n '(sk_live_|sk-[A-Za-z0-9]{20}|AKIA[0-9A-Z]{16}|ghp_[0-9A-Za-z]{36}|AIza[0-9A-Za-z_\-]{35})' --glob '*.mcp.json' --glob '.mcp.json' --glob '*.env' --glob '**/agent*config*' --glob '.github/workflows/*'
# Swallowed error (empty catch) / unbounded external call (no timeout arg)
rg -n 'catch\s*\([^)]*\)\s*\{\s*\}|except[^:]*:\s*pass' .
Stack-specific patterns (the greps name several ecosystems side by side on purpose): keep the ones for the stack in front of you; a hit means read that line, not auto-fix.
Output format
- A one-line per-surface confirmation for the change, e.g.
endpoint: authz ✓ tenant-scope ✓ rate-limit wired ✓ validation ✓, naming the file:line where each control lives (or n/a — <reason>).
- The backstop grep(s) run for the touched surface(s), with their result (zero hits, or the hit + how it was resolved).
- For any control that is genuinely out of scope, an explicit note saying so — never a silent omission.
Gotcha
- Defined-but-not-wired is the signature AI failure: the rate-limit middleware or CSRF guard is written but never attached to the route. The check is "is it wired on THIS path?", not "does the code exist somewhere?".
- Semantically-valid-but-ignored: an IaC attribute that the provider silently ignores (a made-up
encrypted = true on a resource that doesn't support it) reads as a present control but does nothing — a scanner catches it, a code read does not.
- A green grep is necessary, not sufficient — it proves the obvious anti-pattern is absent, not that the control is correct. Read the seam for anything security-sensitive.
- This checklist routes; it does not replace the deep skill. For an auth/billing/tenant/secret path,
security-sensitive-stop still requires a threat pass before editing.
- Run it per surface, not once at the end. In a multi-file / agentic change the omission compounds — each new endpoint, query, or fetch is its own blind spot. Re-run the matching row when each surface lands, not as a single sweep after the whole feature is "done" (by then the missing control is buried under later diffs).
Do NOT
- Do NOT treat "the prompt didn't ask for it" as a reason to omit a control — that is the exact blind spot.
- Do NOT mark a change done on a green happy-path test alone.
- Do NOT auto-rewrite a grep hit without reading it — some are intentional and belong in an allow-list with a reason.
- Do NOT duplicate the deep skills here — route to them.
Auto-trigger keywords
- invisible controls
- ai code blindspots
- pre-ship checklist
- what did the AI omit
- cross-cutting controls
See also
1---2name: ai-code-blindspots3description: Before finishing any code (endpoint, query, migration, render, file, infra, dependency, test) — the senior pre-ship checklist of invisible cross-cutting controls AI omits, with backstop greps4---56# ai-code-blindspots78AI writes the code the prompt literally asked for and stops. The controls a senior adds from unstated context — authorization, tenant scope, validation, transactions, encryption, rate limiting, output encoding, error handling, edge cases, package existence — are *invisible* to a single-file prompt, so they are the first thing dropped. They are also the classes pattern-linters miss. This skill is the pre-ship checklist: for the surface you just wrote, assert the matching control is present, run the backstop grep, and route to the deep skill when it needs more than a check.910This is a **routing + verification** layer — it does not re-teach what the deep skills own. It is the "did I supply the invisible controls?" gate that `senior-engineering-discipline` points at.1112## When to use1314- You have written (or are about to finish) an endpoint, query, migration, UI render, file/network op, infra resource, dependency change, or test — before you call it done.15- Reviewing an AI-authored diff and asking "what did it silently omit?".16- The `senior-engineering-discipline` rule fired and routed you here.1718Do NOT use when: the change is prose/docs/config-only, or a one-line rename — there is no code surface to check.1920## The Iron Law2122```23FOR EVERY SURFACE YOU TOUCHED, NAME THE INVISIBLE CONTROL AND CONFIRM IT IS PRESENT.24"NOT MENTIONED IN THE PROMPT" IS EXACTLY WHY IT IS MISSING — CHECK IT ANYWAY.25A CONTROL DEFINED BUT NOT WIRED IS ABSENT. A grep IS CHEAPER THAN A BREACH.26```2728## Procedure29301. **Inspect** the change and list the surfaces it touched (endpoint, query, migration, render, file/fetch, infra, dependency, test) — analyze what you actually wrote before checking it.312. For each surface, take its row below: assert every listed invisible control is present (with file:line), and run the matching backstop grep.323. For anything a grep flags or a check can't confirm, open the routed deep skill.334. Emit the per-surface confirmation (Output format); surface any unresolved gap to the user — never silently omit.3435## Surface → invisible controls → backstop3637Run the row(s) for what you touched. The grep is a fast authoring-time backstop (not a substitute for reading the code); zero results is the pass, a hit is a prompt to look.3839| Surface you wrote | Invisible controls to assert | Deep skill |40|---|---|---|41| **HTTP endpoint / route** | authorization (this principal may act on this resource — not just authenticated); tenant scope; **the three negative tests** (unauth→401, non-owner→403/404, cross-tenant→403/404); input validation at the boundary; rate limit actually wired; state-changing → CSRF + audit log | `broken-access-control`, `authz-review`, `threat-modeling` |42| **DB query / ORM** | parameterized, never string-built; tenant predicate present; no `SELECT *` across a serialization boundary; N+1 avoided (eager load) | `source-discovery`, `security` |43| **Migration** | reversible (`down`); expand-contract for drop/rename (never a bare `DROP COLUMN` before code stops reading it); transaction; index on new FK/filter column | `engineering-safety-floor`, `migration-architect` |44| **User-controlled render (FE)** | output-encoded; no `dangerouslySetInnerHTML`/`v-html`/`innerHTML`/`eval` on non-constant input; no secret/token in client code; token not in `localStorage`. **This surface also carries the four completeness rows below — they are part of it, not an optional extra.** | `frontend-render-security` |45| **Third-party asset in shipped markup / CSS** | delivery is self-hosted through the project's own route (framework font/asset primitive, bundled package, or locally-served file) — a font/icon/stylesheet CDN link transmits the **visitor's IP** to that third party on every page view; a hotlink only on a stated consumer opt-in; `integrity` + `crossorigin` if a CDN link stays | `design-fidelity-mechanics` § Asset & imagery discipline (owner), `typography-system` |46| **File / outbound fetch** | path confined to an allowed base; SSRF allow-list + private-IP block on user-supplied URLs; size limit; **explicit timeout** (no unbounded/default-infinite wait); server-side validation (never client-only) | `security`, `defense-in-depth` |47| **Infra / IaC** | least-privilege (no `Action:*`/`Resource:*`); encryption at rest; no `0.0.0.0/0` to mgmt/DB ports; no hardcoded creds; scanner-verified, not `plan`-verified | `engineering-safety-floor`, `terraform`, `secrets-management` |48| **New dependency** | package exists on the real registry (not hallucinated); not typo-adjacent to a popular one; pinned + lockfile committed; CVE-scanned | `supply-chain-intake`, `dependency-upgrade` |49| **Secrets/credentials** | never a literal in source; env / secret-manager reference; password columns use bcrypt/argon2, never MD5/SHA; **agent/MCP config too** — no raw key in `.mcp.json` / agent-config / CI YAML / a committed `.env` (env-var indirection, gitignored) | `secrets-management`, `domain-safety-pii` |50| **Error path / timeout / retry** | failure handled, never silently swallowed (no empty `catch {}`); external call has a bounded timeout; retry is capped + backed-off + idempotent (no retry storm); the error surfaced to the caller carries no secret / stack / PII; a partial failure leaves consistent state (no half-written record) | `systematic-debugging`, `defense-in-depth` |51| **Concurrency / shared state** | check-then-act guarded (DB lock / atomic op / transaction — not a read-modify-write race); retried write carries an idempotency key; no unbounded parallel fan-out; shared mutable state synchronized | `source-discovery`, `security` |52| **Test** | asserts general behavior; expected derived from inputs/seeded data, not hardcoded; boundary + error + abuse cases, not only happy path | `testing-anti-patterns`, `test-driven-development` |5354### Render surface — the completeness rows5556Every row above is a **security** control. These four are the **completeness**57controls for the same surface — the states an agent most reliably omits, because58the prompt named the happy path and nothing else. A render that ships only the59populated state is not finished; it is one of four states written.6061**The grep polarity is INVERTED here, and reading it the other way is the failure62mode this table has.** Above, zero hits is the pass. Below, **zero hits next to a63render that fetches or iterates is the prompt to look** — the state is probably64absent. A hit is not a pass either: the column is a `heuristic`, a locator for65code to read, never proof the state behaves. A framework abstraction can66implement all four correctly and match none of these patterns, and a codebase can67match all four while shipping a broken empty state.6869| State | Assert on the render you wrote | Backstop grep (heuristic — zero hits is the prompt) |70|---|---|---|71| **Empty** | the zero-rows / no-results case renders a deliberate state, not a bare frame or a collapsed layout; it says what is missing and what the user can do next | `rg -n -e '\.length\s*[=<]' -e '\bisEmpty\b' -e 'count\(\)\s*[=<]' -e '\bempty\b'` — over the file that maps the collection |72| **Loading** | the pending case renders (skeleton, spinner, disabled control); no layout shift on arrival; the state is reachable from the actual async call, not only defined | `rg -n -e 'isLoading' -e 'isPending' -e 'isFetching' -e 'Skeleton' -e 'Spinner' -e 'aria-busy'` |73| **Error** | the failed case renders a recoverable state with a retry or a next step; the message carries no stack, secret or PII (the `frontend-render-security` row owns that half) | `rg -n -e 'isError' -e 'onError' -e 'ErrorBoundary' -e '\.catch\(' -e 'role="alert"'` |74| **Keyboard path** | every interactive element is reachable and operable by keyboard alone, with a visible focus ring; a click handler on a non-button element carries a role, `tabindex` and a key handler | `rg -n -e 'onClick' -e '@click' -e 'v-on:click'` lists the handlers, then `rg -n -e 'tabIndex' -e 'tabindex' -e 'onKeyDown' -e '@keydown' -e 'role='` on the same file — a handler with no match on the second is the prompt |7576Route to `accessibility-auditor` for the keyboard row when the surface is more77than a handful of controls — this row asserts the path exists, it is not a WCAG78pass.7980### Backstop greps (authoring-time, cross-stack)8182```bash83# Frontend insecure render / client secrets (React + Vue + vanilla)84rg -n 'dangerouslySetInnerHTML|v-html|\.innerHTML\s*=|\beval\(|new Function\('85rg -n 'NEXT_PUBLIC_.*(SECRET|KEY|TOKEN)|VITE_.*(SECRET|KEY)|localStorage\.setItem\([^)]*[Tt]oken'86# String-built SQL (concatenation / interpolation into a query)87rg -n 'query\(.*(\+|\$\{|`).*\)|(SELECT|INSERT|UPDATE|DELETE).*(\+|\$\{)'88# Hardcoded secrets / weak hashing89rg -n 'AKIA[0-9A-Z]{16}|sk_live_|AIza[0-9A-Za-z_\-]{35}|password\s*=\s*["\x27]|md5\(|sha1\(' .90# Infra: wildcard IAM / open ingress / disabled TLS91rg -n '"Action"\s*:\s*"\*"|"Resource"\s*:\s*"\*"|0\.0\.0\.0/0|InsecureSkipVerify|verify=false|curl.*\| *bash' .92# Raw secret in an agent/MCP/CI config (should be an env-var reference, not a literal)93rg -n '(sk_live_|sk-[A-Za-z0-9]{20}|AKIA[0-9A-Z]{16}|ghp_[0-9A-Za-z]{36}|AIza[0-9A-Za-z_\-]{35})' --glob '*.mcp.json' --glob '.mcp.json' --glob '*.env' --glob '**/agent*config*' --glob '.github/workflows/*'94# Swallowed error (empty catch) / unbounded external call (no timeout arg)95rg -n 'catch\s*\([^)]*\)\s*\{\s*\}|except[^:]*:\s*pass' .96```9798Stack-specific patterns (the greps name several ecosystems side by side on purpose): keep the ones for the stack in front of you; a hit means read that line, not auto-fix.99100## Output format1011021. A one-line per-surface confirmation for the change, e.g. `endpoint: authz ✓ tenant-scope ✓ rate-limit wired ✓ validation ✓`, naming the file:line where each control lives (or `n/a — <reason>`).1032. The backstop grep(s) run for the touched surface(s), with their result (zero hits, or the hit + how it was resolved).1043. For any control that is genuinely out of scope, an explicit note saying so — never a silent omission.105106## Gotcha107108- **Defined-but-not-wired** is the signature AI failure: the rate-limit middleware or CSRF guard is *written* but never attached to the route. The check is "is it wired on THIS path?", not "does the code exist somewhere?".109- **Semantically-valid-but-ignored**: an IaC attribute that the provider silently ignores (a made-up `encrypted = true` on a resource that doesn't support it) *reads* as a present control but does nothing — a scanner catches it, a code read does not.110- A green grep is necessary, not sufficient — it proves the obvious anti-pattern is absent, not that the control is correct. Read the seam for anything security-sensitive.111- This checklist routes; it does not replace the deep skill. For an auth/billing/tenant/secret path, `security-sensitive-stop` still requires a threat pass *before* editing.112- **Run it per surface, not once at the end.** In a multi-file / agentic change the omission compounds — each new endpoint, query, or fetch is its own blind spot. Re-run the matching row when each surface lands, not as a single sweep after the whole feature is "done" (by then the missing control is buried under later diffs).113114## Do NOT115116- Do NOT treat "the prompt didn't ask for it" as a reason to omit a control — that is the exact blind spot.117- Do NOT mark a change done on a green happy-path test alone.118- Do NOT auto-rewrite a grep hit without reading it — some are intentional and belong in an allow-list with a reason.119- Do NOT duplicate the deep skills here — route to them.120121## Auto-trigger keywords122123- invisible controls124- ai code blindspots125- pre-ship checklist126- what did the AI omit127- cross-cutting controls128129## See also130131- [`senior-engineering-discipline`](../../rules/senior-engineering-discipline.md) — the anchor rule that routes here.132- [`frontend-render-security`](../frontend-render-security/SKILL.md), [`supply-chain-intake`](../supply-chain-intake/SKILL.md), [`testing-anti-patterns`](../testing-anti-patterns/SKILL.md).133- [`authz-review`](../authz-review/SKILL.md), [`threat-modeling`](../threat-modeling/SKILL.md), [`defense-in-depth`](../defense-in-depth/SKILL.md), [`source-discovery`](../source-discovery/SKILL.md), [`secrets-management`](../secrets-management/SKILL.md).