TIPS 49 · QUATTRO

It passes the tests, it isn't safe

The prompt I give a second Claude Code session when the tests are green: I ask it to attack the code that was just written, not to approve it, and I take the report back to the first one.

What it is and why it matters

When Claude Code finishes a job and tells you “done, tests passed”, it feels like the end of the story. It isn't. Green tests tell you one thing only: the code does what the tests check. And the tests check what was asked for. If you asked “let me save an order”, the test verifies that the order gets saved. Nobody asked what happens if someone saves it without logging in, or saves it in another user's name.

That's why “it passes the tests” doesn't mean “it's safe”. An app can pass all its tests and meanwhile let one user read another user's data, accept the same payment twice or leave a secret key in the part that runs in the browser, where anyone can read it.

A study posted on arXiv on October 10, 2026, “Last Secure Code Benchmark: Is the Code You Vibe Secure?”, says so too. It tested six of the most advanced coding agents. Their code passes the functional tests in 73.6–91.3% of the tasks, depending on the agent. But even the best of the six writes code that both works and is secure in only 25.1% of the tasks.

Asking the same session to double-check doesn't help much: it just wrote that code and knows why it did it that way. What I do is open a second session and give it a specific role, the attacker. It knows nothing about how the code came about, it sees only the changes and it has one job: find how to break them. I don't ask it whether the code is fine. I ask it where it gives way.

Why it matters: if you build an app without being a developer, you won't spot these holes by rereading the code. And if nobody looks for them, the first to find them is a user.

1. The attacker's prompt

I paste it into the second session as is, when the first one has finished and the tests are green.

The prompt
You are an attacker, not a friendly reviewer. This code passes the tests: I don't care, the tests only check what was asked for. You have just one job: find how to break what was just done. Look only at the changes: run git status and git diff HEAD (it also shows the ones already staged), and read in full the new files that git status lists (the diff doesn't show them). Look for:
- who can call it: what happens without logging in, logged in as another user, calling the address directly;
- bad data: empty fields, extremely long text, strange characters, negative numbers, wrong types, the same request sent twice;
- keys in plain sight: passwords, tokens or secret keys written in the code, in the logs, in error messages or in the part that runs in the browser;
- permissions: who can read, change or delete data that isn't theirs;
- edge cases: empty lists, slow network, two users at the same moment.
For each problem write: where (file and line), how I break it (the exact steps), what happens, severity (high, medium, low). Sort from the most severe. Don't fix anything and don't change the files: I only want the report.

Why it's written this way, line by line.

  • “You are an attacker, not a friendly reviewer”: a friendly reviewer tries to understand the code and in the end tends to give it a pass. An attacker starts from the idea that there's a hole and goes looking for it.
  • “Run git status and git diff HEAD”: plain git diff only shows the changes that aren't staged yet, that is, not yet marked with git add for the next commit. git diff HEAD compares all of them with the last commit, staged or not. Files git isn't tracking yet still don't show up there: git status lists them, and that's why the reviewer reads them in full.
  • “Who can call it”: a page can hide a button from people who don't have permission, but if the address that button calls answers anyone, the hidden button protects nothing. That's why the prompt asks to call the address directly.
  • “How I break it (the exact steps)”: with the steps you can check yourself whether the problem is real, and after the fix repeat them to see whether it's gone. A “there might be a risk” is something you can't verify.
  • “Don't fix anything and don't change the files”: the attacker reports and that's it. The fixes are made by the session that wrote the code, so you always know who changed what.

2. How I use it

  1. The first session says “done, tests passed”. I don't ask it to check itself again.
  2. I open a new terminal in the same folder and run claude: a new session that hasn't seen the conversation.
  3. I paste the prompt before the commit, so git status and git diff HEAD show only the work that was just done. If I've already committed, I tell it which files myself.
  4. I take the report to the first session and have it fix things starting from high severity. For every serious problem I also ask for a test that reproduces it, so next time the test catches it.

From the report to the fixes

I hand the report to the first session with this prompt:

The prompt
Here is the report from another session that tried to break your work: [paste the report]. Start from the high-severity problems, one at a time. For each one, first write a test that reproduces it and check that it fails, then fix it and check that the test passes. Tell me what you changed and how you checked it. Don't touch the low-severity ones: I'll decide on those.

The test that fails before the fix is the piece that matters. It tells you the problem was real and not just the reviewer's idea. And it stays in the project: if a future change reopens the same hole, that test turns red.

If you want to be sure it touches nothing

The prompt says not to change the files, but it's still only a request. If I want a real block, I start the second session in plan mode:

The line
claude --permission-mode plan

In plan mode Claude reads files and runs commands to explore, like git status and git diff HEAD, but doesn't change the code until you approve a plan. If it ends up proposing one with fixes, don't approve it: the report is enough. In a session that's already open, you switch to it by pressing Shift+Tab until “plan mode on” appears at the bottom.

3. Don't explain anything

I don't explain to the reviewer why the code is built this way. If you explain it, it ends up agreeing with you.

That goes for every sentence you add to the prompt: “I did it this way because…”, “that part is temporary”, “login is already handled by another part of the app”. Sentences like these pull the reviewer onto your side before it even looks at the code. Give it the changes and the job, nothing more. You'll need the explanations later, when you decide what to fix.

Common mistakes

  • Reading “tests passed” as “safe”: the tests check what was asked for, so whenever you touch login, permissions or data, always do a round with the attacker.
  • Asking the same session to attack its own code: open a new terminal and run claude, so the attacker isn't the one who wrote it.
  • Running it after the commit without saying which files: git diff HEAD no longer shows the work you just did, so name the files to look at yourself.
  • Explaining to the attacker why the code is built this way: give it only the changes and the job, or it ends up agreeing with you.
  • Fixing without a test: for every serious problem, have a test that reproduces it written first, so if it comes back you'll notice.
  • Fixing everything it finds: start from high severity and decide on the low ones yourself, or the code fills up with useless safeguards.

Checklist

  • The tests are green and the first session is done
  • The attacker is in a new session
  • No explanations added to the prompt
  • Before the commit, or with the files named
  • Every problem has file and line, steps, effect and severity
  • Every high-severity problem is fixed and has its own test

Frequently asked questions

If the tests aren't enough, what are they for?

They're very useful. They tell you that what you asked for works and that the next change won't break it without you noticing. They don't tell you what happens when someone uses the app in a way nobody asked for. The two work together: the attacker finds the hole, the test keeps it closed.

Isn't there a ready-made command for security?

Yes. /security-review analyzes the changes on your branch looking for security vulnerabilities: it compares them with the main branch of the remote repository, so the project needs to have one, on GitHub for example. /code-review, on the other hand, looks for bugs in your current changes. The prompt, though, also covers things that aren't just security, like bad data and edge cases, and gives you the steps to reproduce each problem.

When is it worth doing?

When the change touches login, permissions, user data or payments, or adds an address the app calls from the browser. That's where a hole lands on the people using the app. For a piece of text or a color, you don't need it.

← All guides