# Building a Security Reviewer Subagent

> Source: <https://dev.to/arredondop03/building-a-security-reviewer-subagent-2e27>
> Published: 2026-11-14 21:06:01+00:00

The next lesson in the course is to create a subagent. And now that there's at least one feature (login) working in both the frontend and the backend, it's a good time to run a security audit using a subagent dedicated to that.

A subagent is an AI assistant that has specific instructions for completing tasks and can run in the background, so it doesn't interrupt the main thread, and when it's done, it reports its findings or the changes that it made. I had the [security-reviewer](https://github.com/arredondop03/cc-outreach-be/blob/main/.claude/agents/security-reviewer.md) subagents report the findings of the security audit. I reviewed the reports and made decisions on what to implement and what to ignore. Some of the benefits of a subagent over entering a regular prompt for the main agent are that I don't have to repeat all the specific instructions every time and it produces a more consistent and predictable output.

Building the subagents was a lot easier than I expected. The agent creation wizard was removed, so now the only way to build a subagent is by sending a regular prompt to the main agent instructing it to create one along with the it's purpose. Claude generates the agent file in a specific folder, depending on [the scope](https://code.claude.com/docs/en/sub-agents#choose-the-subagent-scope). The way I used it this time was by explicitly asking the main agent to use the subagent.

**Medium**

**No CSP or security headers.** index.html has no CSP and the repo has no hosting config. If an XSS bug shows up later, nothing limits the damage, and the logged-in app could be framed (clickjacking). Set these at the host:

frame-ancestors only works as a real header, not in a meta tag.

```
💡 Decision: Add as a task in the implementation plan - CSP can’t be written until the API address exists.
```

**Low**

`LoginPage.tsx:23`, set in `RequireAuth.tsx:15-19`). It isn't exploitable today, because `state.from` is a router location object that a link or query string can't set. It would become a problem if you switch to a `?next=` param. In that case, accept only paths that start with `/` and not `//`, and that contain no `\`.

```
💡 Decision: Ignore - I’m not using the `?next=` param to redirect after login.
```

`shadcn` is in `dependencies`.` npm audit` findings (2 low, 4 moderate). Those are build-time only and don't ship in the browser bundle. Move it to `devDependencies`.

```
💡 Decision: Implement - Self explanatory.
```

`cn@^0.5.2` is an unusual package.`AppShell.tsx:1` imports it where the usual helper is `clsx` + `tailwind-merge`. Check it's the package you meant, or swap it. All dependencies use `^` ranges, so install with `npm ci`.

```
💡 Decision: Ignore - It’s a replacement for `clsx` + `tailwind-merge`.
```

**Checked and clean**

`dangerouslySetInnerHTML`, `eval` or dynamic `href`/` src` in `src/`.`/login` sits under `RequireAuth`.` console.*` calls, and no direct `fetch` calls.`.env` is gitignored, and the only `VITE_*` variable is the API URL.`src/app.ts:18`, `src/lib/auth.ts:6-15`). The reviewer believes it is on by default only in production and keeps its counts in memory, but didn't check that against the docs. Fix: set `rateLimit` in `betterAuth()` with a strict rule for `/sign-in/email` (for example 5 per minute).

```
💡 Decision: Implement - Even though it’s not an issue at the moment, I wanted to make sure that route has an extra protection and it’s a quick fix.
```

`trust proxy` isn't set`src/app.ts:13`), and you deploy behind a proxy. Client IPs will be wrong, and a spoofed `X-Forwarded-For` could dodge IP-based limits. Fix: `app.set('trust proxy', 1)` and set the IP header explicitly in Better Auth.

```
💡 Decision: Add as a task in the implementation plan - Can’t test until deployment.
```

`helmet` isn't used (`src/app.ts:14-16`). Fix: add `helmet()`. HSTS is the main benefit.

```
💡 Decision: Implement - Agree with the main benefit listed.
```

`BETTER_AUTH_URL` and `FRONTEND_ORIGIN` default to localhost, even in production. A missed variable would fail quietly.

```
💡 Decision: Ignore - Don’t have production URLs, will update after deployment.
```

`X-Request-Id` is trusted without length or format limits (`src/middleware/requestLogger.ts:8-10`). Pino escapes it, so there's no log injection. Restrict it to `/^[\w-]{1,64}$/`.

```
💡 Decision: Implement - It’s a one-line change that keeps junk out of my logs.
```

`HttpError.details` are returned as-is (`src/middleware/errorHandler.ts:18,25`). A future error with internal wording could leak. Allow-list the status codes.

```
💡 Decision: Ignore - It’ll be an issue when I integrate third party APIs. Will worry about this then.
```

`/openapi.json` is public (`src/app.ts:25`). Impact is minimal today, but gate it once real routes exist.

```
💡 Decision: Implement - The next change in the backend will be adding real routes.
```

**Clean:**

Something I noticed was that the security-reviewer would bring up concerns about possible future problems. I will run the subagent a few more times to see if it keeps doing it. If that’s the case, I will update the instructions to only report the vulnerabilities as the code stands today.

Frontend Repo: [cc-outreach-fe](https://github.com/arredondop03/cc-outreach-fe)

Backend Repo: [cc-outreach-be](https://github.com/arredondop03/cc-outreach-be)
