A read-only AI security reviewer
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.
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.
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.
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.