How I Use Claude Code to Fix Security Vulnerabilities

About this article
This article documents how I use Claude Code in my CVE research workflow — specifically, how I handle the moment Claude gives me a fix recommendation for a vulnerability I've found through static analysis.
Over many sessions I noticed a recurring pattern: when I ask Claude to patch a vuln, it often returns 2–3 options instead of one answer. At first this looked like a weakness — I just wanted "the fix." After enough real cases, I realized it's actually a useful behavior most of the time (real trade-offs exist in security work), and a footgun the rest of the time (Claude hedging because my prompt was vague).
What follows is the framework I now use to work with Claude on security fixes:
§1–§2 — Why multiple-option answers happen and the 5 trade-off shapes I see most often
§3 — A 5-axis evaluation framework for picking the right option
§4–§5 — How to tell genuine trade-offs from Claude hedging
§6 — The step-by-step workflow I run when Claude gives me options
§7 — How I write prompts that prevent hedging in the first place
If you're using AI to help with security fixes — whether for CVE research, internal pentest reports, or just routine vulnerability triage — this should save you some of the trial and error it took me to figure out.
1. The reality: most security fixes don't have "one right answer"
Unlike ordinary bug fixes (broken logic → patch it), security fixes usually have multiple valid approaches because:
Each fix lives at a different layer of the pipeline
Each fix has different trade-offs (security strength vs UX vs compatibility vs performance)
Claude doesn't know what the project prioritizes → it offers options so you can decide
This isn't Claude being lazy or unsure — it's Claude respecting your domain knowledge about the project's context.
2. Common trade-offs when Claude offers 2 options
A. Minimal fix vs Defense in depth
Example — Stored XSS in a Twig template:
// Vulnerable
{{ user.bio | raw }}
Option A — Minimal (fix at the sink):
{{ user.bio }} // remove |raw, let Twig auto-escape
Smallest diff, easiest to review
No change to data structure
If another template also renders
bio→ still vulnerable
Option B — Defense in depth (sanitize at input + escape at output):
// In the controller (input)
\(user->bio = strip_tags(\)request->bio, '<b><i><a>');
// In the template (output)
{{ user.bio | raw }} // data is now clean
Protection at 2 layers — safe even if
|rawis accidentally added elsewhereLarger diff, touches input validation
If
strip_tagsstrips too aggressively → user loses data
Claude offers 2 options because:
Security-conscious maintainers usually prefer B (defense in depth)
Maintainers who prioritize a small, easy-to-review PR prefer A
Claude doesn't know the project's culture
B. Library/framework approach vs Manual approach
Example — SQL Injection in a raw PHP query:
// Vulnerable
\(db->query("SELECT * FROM users WHERE email = '\)email'");
Option A — Use the framework's ORM/Query Builder:
// Laravel Eloquent
User::where('email', $email)->first();
Idiomatic for a Laravel codebase
Auto-parameterized
If raw queries are used elsewhere → inconsistent
Option B — Plain PDO prepared statement:
\(stmt = \)db->prepare("SELECT * FROM users WHERE email = ?");
\(stmt->execute([\)email]);
Minimal change — still raw SQL, just adds binding
No ORM knowledge required
If the project already uses ORM → goes against the established pattern
Claude offers 2 options because it can't tell the project's idiomatic style — reading 1-2 files isn't enough to be sure.
C. Strict vs Permissive validation
Example — SSRF in a webhook URL:
// Vulnerable
\(response = file_get_contents(\)request->webhook_url);
Option A — Allowlist (strict):
$allowed_hosts = ['api.partner-a.com', 'webhook.partner-b.com'];
\(host = parse_url(\)url, PHP_URL_HOST);
if (!in_array(\(host, \)allowed_hosts)) {
throw new Exception('Host not allowed');
}
Most secure — only pre-approved hosts allowed
Kills the "arbitrary webhook" feature — poor UX
Option B — Blocklist (block private IP ranges):
\(ip = gethostbyname(parse_url(\)url, PHP_URL_HOST));
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
throw new Exception('Private IP not allowed');
}
Keeps arbitrary-webhook functionality
Blocks 99% of common SSRF attacks
DNS rebinding can still bypass if you don't re-resolve
Claude offers 2 options because this is a product decision, not a pure technical decision. "Webhook" means different things for different products.
D. Reject vs Sanitize vs Encode
Example — XSS in a comment system:
Option A — Reject (block input):
if (preg_match('/<script|on\w+=/i', $comment)) {
throw new Exception('Invalid input');
}
Option B — Sanitize (clean input):
\(comment = HTMLPurifier::clean(\)comment); // strip dangerous tags, keep safe HTML
Option C — Encode (output safely):
echo htmlspecialchars($comment); // keep raw input, escape when rendering
Trade-off:
| Option A | Option B | Option C | |
|---|---|---|---|
| UX | Poor (false rejects) | Medium | Best |
| Security | Weak (regex bypass) | Strong | Strongest |
| Performance | Fast | Slow (HTMLPurifier is heavy) | Fast |
| Implementation cost | Low | High (new dependency) | Low |
This is a classic security architecture decision — there's no right or wrong, only what fits the context.
E. Breaking change vs Backward compatible
Example — JWT auth bypass via alg: none:
# Vulnerable
jwt.decode(token, key, algorithms=['HS256', 'RS256', 'none'])
Option A — Breaking (accept only one algorithm):
jwt.decode(token, key, algorithms=['RS256']) # drops 'none' and HS256
Most secure
Existing HS256 tokens stored in clients will fail → mass re-login required
Option B — Non-breaking (keep HS256, drop 'none'):
jwt.decode(token, key, algorithms=['HS256', 'RS256'])
Fixes the vuln, breaks nothing
HS256 still carries risk if the secret is leaked
Claude offers 2 options because it doesn't know:
How many users hold HS256 tokens
Whether mass re-login is acceptable
Whether a key rotation mechanism exists
3. How to read Claude's options — an evaluation framework
When Claude offers 2-3 options, evaluate along 5 axes:
Security strength — Which option closes the hole more tightly?
Diff size — Which is easier to review?
Compatibility risk — Which might break users/clients?
Idiomatic with the project — Which matches the codebase's style?
Maintainability — Will a new developer understand this 6 months from now?
Rules of thumb for responsible disclosure PRs:
| Situation | Pick |
|---|---|
| First PR to a large project | Minimal fix — easier to accept; you can follow up with a broader PR |
| Critical vuln, exploit already in the wild | Defense in depth — close as many layers as possible |
| Project with a clear style guide | Idiomatic option — even if the diff is larger |
| Library/SDK with many downstream consumers | Non-breaking option — avoid forcing mass migration |
4. When Claude SHOULDN'T offer 2 options (an anti-pattern)
Sometimes Claude offers 2 options but only one is actually correct — this is a sign Claude is hedging due to lack of confidence:
Option A: use
mysqli_real_escape_stringOption B: use prepared statements
This isn't a real trade-off — prepared statements are always better. Claude offered A out of caution. You should push Claude to pick one and explain.
Option A: hash passwords with MD5
Option B: hash passwords with bcrypt
MD5 for passwords is never acceptable. This isn't a choice.
When you suspect Claude is hedging:
Ask: "If you had to pick one, which would you choose and why?"
Ask: "Is there industry consensus on this?"
Check the OWASP cheat sheet — if there's a clear recommendation → follow it
5. When offering 2 options actually has value
It has value when:
The 2 options sit at different layers (input vs output, framework vs manual)
The 2 options carry obvious trade-offs that anyone can see
The 2 options require domain knowledge you have but Claude doesn't (business logic, user base, deployment context)
The project maintainer has a strong opinion that Claude isn't aware of
It has no value when:
The 2 options differ only in syntax
One of the 2 options is clearly less secure
Claude is offering them just to hedge against uncertainty
6. A workflow for handling 2-option recommendations
Step 1: Read the trade-off Claude presents
"Option A: minimal, Option B: defense in depth"
→ Is this trade-off real?
Step 2: Map it onto the project's context
- Does the project have a strong security culture? (read SECURITY.md, CHANGELOG)
- Does it have a style guide? (read CONTRIBUTING.md)
- What kind of PRs does the maintainer typically accept? (read recent merged PRs)
Step 3: Ask Claude again, this time with context
"This project uses idiomatic Laravel with Sanctum for auth. Given that, which option do you pick?"
Step 4: Decide
- First PR: go minimal
- Already discussed with the maintainer: go with what they want
Step 5: Document the choice in the PR description
"Considered Option B (defense in depth) but chose A for minimal diff. Happy to extend if maintainer prefers."
→ Shows you thought about it — not a copy-paste
7. Writing better prompts to reduce hedging in the first place
Section 4 covered how to spot hedging after it appears. Better: prevent it. Most hedging traces to three gaps — no constraints, no project context, or you explicitly asked for options.
7.1 Prompts that work
Force a decision with constraints:
❌ "How should I fix this XSS?"
✅ "Fix this XSS with the smallest diff. First-time PR to a project I don't maintain. Pick one and apply it."
Bake context into the prompt:
❌ "Fix the SSRF in
webhook.php."✅ "Fix the SSRF in
webhook.php. B2B integration platform — allowlist won't work. Block private IPs with the right PHP API. Apply the fix."
Ask for a recommendation, not options:
❌ "What are my options for fixing this JWT bypass?"
✅ "Recommend the single best fix. Mention the trade-off you rejected in one sentence."
Set a confidence threshold:
✅ "Pick one fix and apply it. Mention an alternative only if it's 30% better on security, maintainability, or performance."
7.2 A reusable template
Fix [vulnerability class] in [file].
Context: [framework, scale, audience — 1 sentence].
Constraints: [smallest diff / defense in depth / breaking-change OK?].
Decision: pick one fix and apply it. Alternatives only if substantially better.
7.3 Stop phrases that invite hedging
| Stop phrase | Use instead |
|---|---|
| "What are my options" | "Recommend one" |
| "How could I fix this" | "Apply the best fix" |
| "What do you think?" | "Pick one and explain" |
| "Compare X and Y" | "Which is right here, and why?" |
7.4 When you DO want options
Sometimes the trade-off is a real product decision (e.g., Section 2.C). Ask explicitly and bound the answer:
✅ "Give me exactly two fixes: one allowlist, one blocklist. For each: security strength, UX impact, one failure mode. No third option."
8. What this workflow has produced
Published advisories
- GHSA-6jq6-x4cx-qvcm — in
[Firefly III]
Pull Request
SQL Injection — in
[Cacti]Cross-Site Scripting (XSS) stored — in
[Cacti]Cross-Site Scripting (XSS) stored — in
[Akaunting]Cross-Site Scripting (XSS) stored — in
[Bagisto]
TL;DR
Claude offers more than one options because security fixes have real trade-offs
5 common trade-off categories: minimal vs defense-in-depth, library vs manual, strict vs permissive, reject vs sanitize vs encode, breaking vs backward-compat
Evaluate options along 5 axes: security, diff size, compatibility, idiomatic fit, maintainability
Watch for hedging — when Claude offers 2 options but one is clearly wrong, push it to pick one
The best prevention is upstream — write prompts that constrain and decide (see §7)
For a first disclosure PR: default to the minimal fix — easier to accept, less likely to spark debate
Always document in the PR why you picked that option — maintainers respect a reasoned choice





