Random Thoughts

Developer tooling

A read-only AI security reviewer

Tuesday, April 28, 2026

  • ai-assisted
  • #ai
  • #ai-agents
  • #vibecoding
  • #security
  • #code-review
  • #yaml
  • #markdown
  • #owasp
  • #cursor
  • #claude

The previous post laid out what subagents are. This one is a walkthrough of one I actually use, day to day, on every change that touches anything sensitive: the security-review subagent.

It’s a single Markdown file. Twenty lines of frontmatter and prompt, plus a folder of rule documents it consults. The whole apparatus fits in a small wrapper repo that I add to the workspace alongside whatever project I’m working on. Once it’s there, asking the parent agent “review this for security” spawns the subagent, which reads the relevant rules, compares the implementation against them, and returns a severity-ranked report with citations.

Linocut block print on cream paper in deep red and black ink, with the characteristic edge texture of hand-carved relief printing — visible chisel marks, slightly imperfect edges, high-contrast positive and negative space. Centered on the print is a bold geometric heater-shield silhouette in deep red ink, its outline cut with confident bold strokes and inner cross-hatch detail giving it heft. Inside the shield, instead of any imagery, a stack of horizontal black ink bars represents lines of code — short, medium, longer, then short again — with no readable text. To the right of the shield, a vertical column of four small geometric markers ranks downward in visual emphasis: a heavy filled square in deep red (largest, most assertive), a solid red diamond, a horizontal red-and-black hatched bar, and a small open black circle (smallest). A thin black ink line connects each marker back to the shield's right edge. Just below the shield, a small ink-stamped padlock motif in black with a single white-cut stripe slicing across it indicates a closed lock. Bold woodblock line work, characteristic linocut grain, two-color print on cream paper. No readable text or letters anywhere in the composition.
Read the code. Sort by gravity. Cite the rule. Don't touch anything.

Here’s what’s inside, why it works, and the choices that distinguish a useful security reviewer from a noisy one.

The definition file

The whole subagent lives in a single file:

---
name: org-standards-security-review
description: >
  Security and privacy review against the org standards in
  standards/rules/. Use when implementing or reviewing auth,
  APIs, data handling, uploads, crypto, sessions, or infra.
  Use proactively for sensitive features.
model: inherit
readonly: true
---

You review work against the org standards in `standards/rules/`.

## When invoked

1. From the parent prompt, infer scope (features, files, or APIs).
2. Identify which topics under `standards/rules/` apply.
   Examples: CSRF and auth tokens for state-changing HTTP;
   parameterized DB queries for SQL; hardcoded-secrets and
   sensitive-data-* for credentials and logs;
   file-upload-validation for uploads.
3. Read the relevant `standards/rules/*.md` files and compare
   the implementation to the written rules. Cite filenames in
   findings.
4. Report by severity (Critical / High / Medium / Low). Each
   finding must reference a rule file.

Do not invent stricter requirements than the written rules.
If the rules are silent or ambiguous, say so explicitly.

That’s it. Twenty-something lines. The important choices are the description, readonly: true, and the guardrail at the bottom.

The description is not decorative. It is the trigger surface the parent agent pattern-matches against: auth, APIs, data handling, uploads, crypto, sessions, infra. The phrase “Use proactively for sensitive features” tells the parent it does not need to wait for me to ask.

readonly: true is the line that changes how I use it. A read-only subagent can read, search, navigate, and summarize, but the result is purely a report. I do not have to review what it is about to do before it does it; I just review what it found. For review work, that is the right default.

Linocut block print on cream paper in deep red and black ink, with characteristic chisel-mark texture and slightly imperfect carved edges. Centered on the page is a bold rectangular paper outline cut in heavy black ink, representing a single document. Inside the document outline, four horizontal bands stack vertically, each band filled with short black hatch-marks suggesting lines of dense text without spelling anything. To the left of each band, a thin black ink line extends out into the margin and ends in a distinctive small carved symbol: the first band's annotation ends in a small five-pointed star, the second in a small eye-shape, the third in a small gear, and the fourth in a small padlock. The padlock annotation is rendered larger and printed in deep red ink — the only red on the print — clearly emphasized over the other three black annotations. Bold woodcut line work, characteristic linocut grain, two-color print on cream paper. No readable text or letters anywhere in the composition.
Twenty lines of carved structure. Each annotation answers a different question; the red one is the one that changes how you'll use it.

The body — what the subagent actually does

The system prompt is short on purpose. Four numbered steps, plus a guardrail.

Step two: identify which rules apply. This is where the subagent earns its keep. The rule directory has dozens of files, each covering one topic — CSRF, auth tokens, parameterized DB queries, secrets handling, file uploads, dependency scanning, and so on. The parent agent doesn’t want to load all of those. The subagent — having scoped the change — picks the relevant ones.

The prompt gives examples to anchor this: “CSRF and auth tokens for state-changing HTTP; parameterized DB queries for SQL; hardcoded-secrets and sensitive-data- for credentials and logs.”* These are seeds. The subagent extrapolates from them based on what it sees in the change.

Step three: read the rules and compare. The subagent opens each relevant rule file and compares the implementation. The crucial detail is “Cite filenames in findings.” Every finding the subagent reports has to point at a specific rule it’s citing. That citation is what makes the output verifiable. I can open the rule the subagent referenced, read it myself, and decide whether the finding is correct.

The guardrail at the bottom — “Do not invent stricter requirements than the written rules. If the rules are silent or ambiguous, say so explicitly” — is the line that prevents the subagent from going off the rails. Without it, the subagent will sometimes produce findings based on what it generally believes about security, not on what’s actually in the rule files. That’s worse than useless: it undermines the citations and inflates the report with claims I can’t trace back to anything.

The rule directory the subagent reads

The subagent’s behavior is only as good as the rules it consults. The directory looks roughly like this:

standards/rules/
├── auth-tokens-expiration.md
├── csrf-protection.md
├── csp-headers.md
├── dependency-vulnerability-scanning.md
├── error-handling-with-no-silent-failures.md
├── file-upload-validation.md
├── hardcoded-secrets.md
├── input-sanitization.md
├── least-privilege-principle.md
├── parameterized-db-queries.md
├── password-requirements.md
├── predictable-ids.md
├── rate-limiting.md
├── secure-session-management.md
├── security-events-logging.md
├── security-misconfiguration.md
├── sensitive-data-in-client-side.md
├── sensitive-data-in-errors.md
├── sensitive-data-in-logs.md
├── sensitive-data-in-querystring.md
├── ssrf-protection.md
└── validate-all-inputs.md

Each file covers one topic, in plain Markdown, with concrete dos and don’ts. They aren’t novels. Each file is one or two pages: a short statement of the policy, the reasoning, examples of compliant and non-compliant code, common pitfalls.

The subagent loves this format because it can pick up only what it needs. A change to a SQL query loads parameterized-db-queries.md and skips the rest. A file-upload feature loads file-upload-validation.md and validate-all-inputs.md. The subagent’s context stays small, focused, and citable.

(I’ll write more about how the rule directory itself works in tomorrow’s post — including how it’s a git submodule shared across projects, and why making it portable matters.)

What the output actually looks like

A typical run, on a hypothetical small auth change, comes back looking like this:

## Security review — auth.py changes

**Scope:** apps/api/routes/auth.py, apps/api/services/sessions.py
**Rules consulted:** auth-tokens-expiration.md, secure-session-management.md,
  hardcoded-secrets.md, security-events-logging.md

#

## Critical — none

#

## High

1. **apps/api/services/sessions.py:42** — Session token does not
   expire. The current implementation issues tokens with no `exp`
   claim. See `auth-tokens-expiration.md` §"All tokens must have
   a maximum lifetime." Recommend adding an explicit `exp` based
   on the policy in that rule.

#

## Medium

2. **apps/api/routes/auth.py:18** — Failed login attempts are not
   logged. See `security-events-logging.md` §"Authentication
   events." Recommend adding a structured log entry on failure,
   without the credential value.

#

## Low

3. **apps/api/routes/auth.py** — Module-level constant
   `DEFAULT_REDIRECT` is defined but never used. Not a security
   issue; cleanup opportunity. Not cited from any rule.

#

## Notes

- The rules are silent on session token storage location
  (cookie vs localStorage). The implementation uses an
  HTTP-only cookie, which aligns with general best practice
  but is not explicitly required by the consulted rules.

Notice what this output is doing.

Each finding cites a specific rule file. No finding is “from general best practice” — those are explicitly called out as notes, not findings. That separation is what keeps the report trustworthy.

Severity is honest. A “low” finding is genuinely low. The temptation, with any review tool, is to inflate findings to look thorough. The guardrail in the prompt — “Do not invent stricter requirements than the written rules” — pushes back against that.

The “Notes” section captures ambiguity. Where the rules are silent, the subagent says so. It doesn’t pretend the silence is a finding. It just notes the gap. I read those notes and decide whether to update the rules themselves.

The output is short. No introduction, no recap of the change, no encouragement, no closing pleasantry. Severity headers, numbered findings, citations, notes. Everything else got cut.

Linocut block print on cream paper in deep red and black ink, with characteristic hand-carved chisel-mark texture and slightly imperfect edges. The composition is a vertical report-shaped layout printed cleanly on the cream ground. At the top, a thick black ink horizontal bar runs across the print as a header. Below it, a thin secondary band of short horizontal hatches suggests metadata. Then four stacked severity sections fill the middle of the print, each beginning with a distinctive geometric chip cut in ink on its left edge: a deep-red heavy square chip atop a section that contains only a single short black hatch (an empty tier), a deep-red triangle chip atop a section containing one rectangular finding card, a solid black hexagon chip atop a section containing one finding card, and a small open black circle atop a section with one smaller finding card. Each finding card is built from three short stacked black hatch-lines and ends with a tiny black citation-pill bar at its lower edge — no readable text inside the cards. At the very bottom of the print, a thin band of close cross-hatching marks a final notes section. Bold woodcut line work, two-color print on cream paper, visible relief-print grain, no readable text or letters anywhere in the composition.
Severity, finding, citation. Every finding traces to a rule you can open. Ambiguity goes into the notes, not into the findings.

What this catches that I wouldn’t

Three categories of finding consistently come back from this subagent that I’d have missed otherwise.

The thing I forgot to do. A token without an expiration. A log call that includes a credential. A query parameter that holds a session ID. These are all individually obvious, and I individually forget at least one of them per non-trivial change. The subagent doesn’t get tired.

The thing the rules say but I don’t remember. Some rules have specific numeric requirements — minimum password length, maximum token lifetime, required headers. I cannot keep all of those in my head. The subagent reads the rule, sees the number, and tells me when the implementation is off.

The thing nobody noticed before. Sometimes a finding the subagent surfaces points at a real gap in the existing code, not the change. Those are the most valuable findings, because the human review process had been letting them through. The subagent has no incentive to politely overlook them.

What it doesn’t catch

It’s not magic.

Logic-level vulnerabilities. A correctly-implemented auth flow that has a flawed business-logic premise — “anyone with the URL can upgrade their plan” — won’t show up. The rules don’t cover business logic; the subagent won’t invent rules. Logic review is still on humans.

Design-level issues. “You shouldn’t be storing this in a database at all” is a design conversation. The subagent will check that the storage you chose follows the rules; it won’t tell you the design is wrong.

Things the rules don’t cover. The subagent’s only superpower is faithfully consulting the rules. If the rules are missing a category, the subagent will be silent on that category. The remedy isn’t to ask the subagent to be smarter; it’s to write the missing rule.

Why this works as a subagent specifically

This is a subagent rather than a rule or skill because the work is isolated, read-heavy, specialized, and returns a small structured report. That was the shape from the previous post; this is the concrete instance.

That’s the whole pattern. The instance isn’t “smarter than the parent.” It’s the same model in a more focused configuration. The configuration is what does the work.