AgenticCLI
▮ shipguardis a free, local secure-ship agent for AI-built apps — 5 checks in ~2s.$ npm i -g @agenticcli/shipguard

Is Claude Code's Output Secure? I Audited What It Generates

12 min · jul 2026

A founder asked Claude Code to build a Stripe webhook handler last week. It worked. The checkout flow fired, the database updated, the demo looked done. Nobody asked whether the endpoint checked that the request actually came from Stripe. It didn't. Any script that knew the URL could have POSTed a fake checkout.session.completed event and unlocked the paid plan for free.

That's not a story about a bad tool. It's a story about a question nobody asked, because the code ran.

TL;DR

  • Claude Code ships a real, documented security model — read-only by default, sandboxed bash, explicit approval for edits and commands, prompt-injection defenses. That model governs what the agent is allowed to do to your machine. It says nothing about whether the feature it just wrote is secure.
  • We gave a Claude-family model four realistic feature prompts — an auth-gated dashboard, a Stripe webhook handler, a Supabase task API, a file upload endpoint — and scanned each generation with ShipGuard's real, free, local CLI.
  • One came back clean. Three didn't: a webhook with no signature verification, a CRUD API that trusts a client-supplied user id instead of a session, and an upload endpoint with a hardcoded AWS credential fallback.
  • No prompt was adversarial. Nobody asked for insecure code. These are first-draft, ordinary feature requests — the kind that ship every day.
  • Anthropic's own docs are explicit that reviewing the code before approval is the user's job, not the harness's. This piece is what that review actually finds, with the real numbers.

What "Claude Code security concerns" actually means

Search that phrase and you'll mostly find one of two things: writeups of the agent's permission model, or coverage of a specific disclosed vulnerability in the tool itself. Both are worth reading. Neither answers the question most builders actually have, which is simpler and more personal: the feature Claude Code wrote for me an hour ago — is it okay?

Those are genuinely different questions, and it's worth being precise about which one you're asking.

Tool security is about the harness — what Claude Code is allowed to do on your machine while it works. Can it read files outside the project directory? Does it need approval before running a command that modifies your system? Can a webpage it fetches trick it into leaking your source code? Anthropic answers this directly in their own Claude Code security documentation: permission-based architecture with read-only defaults, a sandboxed bash tool with filesystem and network isolation, a working-directory boundary the agent can't write outside of without approval, and specific countermeasures against prompt injection (context-aware analysis, input sanitization, isolated context windows for web fetches). This is real engineering, published plainly, and it's the correct place to look if your worry is "can I trust this thing running on my laptop."

Output security is a different animal entirely: does the code the agent generated — the webhook handler, the API route, the upload endpoint — have a vulnerability in it? This has nothing to do with sandboxing or permissions. A perfectly sandboxed agent, with zero ability to touch anything outside your project folder, can still hand you a Stripe webhook that skips signature verification. The harness did exactly what it was allowed to do. The feature it produced is still broken.

Anthropic's own docs draw this line for you, more bluntly than a vendor usually would about their own product's limits: under "user responsibility," they state plainly that you're responsible for reviewing proposed code and commands for safety before approval. There's also an opt-in security guidance plugin that has Claude review and fix vulnerabilities in its own changes during a session — a real, useful step, and also still the same model grading its own homework rather than an independent check running against fixed rules. Nothing in the harness grades the security properties of what gets generated by default. That's not a criticism of the product. It's a scope boundary, stated in the vendor's own words, and most of what gets written about "Claude Code security" walks right past it.

So we tested the thing nobody was testing: the output, not the harness.

The experiment: four features, one real scanner

Four prompts, each answered independently in its own fresh mock repo, with no shared context between them and no steering toward or away from any particular outcome:

  1. Auth-gated dashboard — a Next.js dashboard page showing account info, gated server-side with Auth.js, with edge middleware protecting the route.
  2. Stripe webhook handler — listens for checkout.session.completed and customer.subscription.deleted, updates the user's plan.
  3. Supabase CRUD API — list, create, update, delete for a personal task list backed by Postgres.
  4. File upload endpoint — a logged-in user uploads a profile image to S3, the URL gets saved to their profile.

These are ordinary asks. No prompt mentioned security, and none was written to bait a specific mistake — the point of this piece falls apart if the generations are strawmen. Each is what it claims to be: a representative completion for that prompt, reproduced with the same model family this article is about, methodology disclosed rather than buried.

Every generation was scanned with the real CLI, no login, no --pro flag — exactly the command and tier available to anyone for free:

npx @agenticcli/shipguard scan --json

A scope note before the results. Three of the four mock repos are narrow, single-feature captures — a fresh repo containing only the files that one prompt would plausibly produce, not a complete app. ShipGuard's free auth check also runs one repo-wide test on any Next.js project: does middleware.ts exist and cover sensitive routes. A real Claude Code session asked only for a webhook handler wouldn't proactively scaffold global route middleware either — nobody asked for that. So that finding fires honestly on three of the four captures below. It's reported where it appears, but it's not double-counted as a bug specific to that feature; each result section calls out what's actually new to that generation.

Feature 1: the auth-gated dashboard — clean

This one came back with nothing to report, and that's worth stating as plainly as the failures: SAFE_TO_SHIP, zero findings, seven files scanned.

The generation did what the prompt asked, all the way through. The dashboard page is a Server Component that calls await auth() and redirects unauthenticated visitors before rendering anything. middleware.ts exists and its matcher explicitly covers /dashboard and /api/account — the prompt asked for edge-layer protection and got it, not just a page-level check. The companion API route for updating the profile also calls auth() and derives the user id from the verified session rather than trusting anything the client sent.

Auth-gating a Next.js route is one of the most heavily represented patterns in the training data any modern coding model has seen — tutorials, docs, and real production code all reinforce the same shape, over and over. This is the pattern working as intended, and it's the honest counterweight to the rest of this piece: modern agent output is frequently good. The rest of this article isn't a claim that Claude Code writes bad code as a rule. It's a claim about variance, and the specific patterns that need checking every time, because the tail exists even when the average is fine.

Feature 2: the Stripe webhook handler — no signature verification

DO_NOT_SHIP. Four findings, two of them critical.

{
  "findings": [
    { "id": "payment_webhook_without_signature_check", "title": "Stripe webhook missing signature verification", "severity": "critical" },
    { "id": "payment_webhook_without_signature_check", "title": "Stripe webhook does not read stripe-signature header", "severity": "critical" },
    { "id": "missing_idempotency", "title": "Webhook handler lacks idempotency protection", "severity": "medium" },
    { "id": "public_admin_route", "title": "No Next.js middleware.ts found", "severity": "high" }
  ],
  "riskScore": { "score": 100, "recommendation": "DO_NOT_SHIP", "totalFindings": 4, "criticalCount": 2 },
  "filesScanned": 3
}

The generation parses the incoming request with req.json() and switches on event.type directly. It never calls stripe.webhooks.constructEvent(). It never reads the stripe-signature header at all. Functionally, the handler works exactly as demoed — send it a JSON body shaped like a Stripe event, and it updates the database. That's also the entire problem: nothing about that flow requires the request to have actually come from Stripe. Anyone who finds the endpoint URL can POST a hand-built checkout.session.completed payload and flip their own account to the paid plan, no payment required.

This is not an exotic mistake. It's the single most common real-world failure mode in AI-generated payment integrations, and it's easy to see why: the naive, first-draft version of "handle this webhook" is "read the body, act on it" — the request has to be authenticated on top of that, and authentication is a step that's invisible in a working demo. The checkout flow fires. The database updates. Nothing about a successful test run tells you the door was never locked. Idempotency — checking whether you've already processed this exact event — got skipped for the same underlying reason: it's invisible until the day Stripe retries a delivery and you double-fulfill an order.

Feature 3: the Supabase task API — auth missing on every mutating route

DO_NOT_SHIP. Three findings, all high severity.

{
  "findings": [
    { "id": "auth_route_without_guard", "title": "Unprotected API route handler: app/api/account/tasks/route.ts", "severity": "high" },
    { "id": "auth_route_without_guard", "title": "Unprotected API route handler: app/api/account/tasks/[id]/route.ts", "severity": "high" },
    { "id": "public_admin_route", "title": "No Next.js middleware.ts found", "severity": "high" }
  ],
  "riskScore": { "score": 90, "recommendation": "DO_NOT_SHIP", "totalFindings": 3 },
  "filesScanned": 4
}

This one is worse than the finding text alone suggests, and it's worth spelling out why. The generation uses a Supabase service-role key on the server — which bypasses Row Level Security entirely, making the API layer the only authorization boundary left — and then never checks a session anywhere. Every handler (list, create, update, delete) takes a userId straight from the query string or the request body and trusts it. There's no auth() call in either file.

That's not just "missing auth" in the abstract. It's an IDOR — an insecure direct object reference — sitting one query-string parameter away from anyone who wants to read, edit, or delete another user's tasks. Change ?userId= to a different value and you're looking at someone else's data. The scanner's finding text says "no auth guard," which is accurate and also somewhat understates the shape of the actual exposure. Reading past the tool's own headline to what the code actually permits is exactly the habit this article is arguing for.

The generation isn't lazy, either — it correctly reaches for a service-role client to get consistent access, which is a reasonable engineering choice on its own. The gap is specifically the layer that was supposed to replace what RLS would have provided: nobody wired a session check back in.

Feature 4: the file upload endpoint — hardcoded fallback credentials

DO_NOT_SHIP. Four findings — three high, one medium.

{
  "findings": [
    { "id": "hardcoded_secret", "title": "AWS Access Key ID", "severity": "high" },
    { "id": "hardcoded_secret", "title": "AWS Secret Access Key", "severity": "high" },
    { "id": "hardcoded_secret", "title": "High-Entropy Secret String", "severity": "medium" },
    { "id": "public_admin_route", "title": "No Next.js middleware.ts found", "severity": "high" }
  ],
  "riskScore": { "score": 100, "recommendation": "DO_NOT_SHIP", "totalFindings": 4 },
  "filesScanned": 3
}

This generation actually got the auth question right — the upload route calls auth(), returns 401 without a session, and scopes the S3 object key to the uploader's own id. What it got wrong was environment hygiene: the S3 client is built with process.env.AWS_ACCESS_KEY_ID || AWS_ACCESS_KEY_ID-style fallbacks, where the fallback constants are hardcoded directly in the route file — a "just so next dev works before you've set up .env.local" convenience that's trivial to forget once the demo is running. (Ours used AWS's own publicly documented example credentials, non-functional by design; the pattern is what matters, and a real key dropped into that same shape ships exactly the same way.)

This is the quiet cousin of the classic "hardcoded API key" story: nobody types a secret into a file on purpose. Someone writes a fallback so the feature works immediately, and the fallback outlives the reason it existed. A scanner doesn't care about intent — it flags the literal string sitting in a file that gets committed to git either way.

Patterns across all four generations

Add it up: one clean generation, three with real, ship-blocking findings, zero adversarial prompts. That's the honest shape of the tail this article is about — not "AI writes insecure code," but "AI writes code with real variance, and the failures cluster in predictable places": authentication that exists at the page level but not the API level underneath it, verification steps that are invisible in a working demo (webhook signatures, session checks on mutating routes), and convenience fallbacks that quietly become the shipped configuration.

None of this is unique to Claude Code, and none of it is a reason to distrust the model generally — it's a reason to distrust unchecked output from any agent, on any given generation. Zico Kolter, who sits on OpenAI's own Safety & Security Committee, makes almost exactly this point about the category, not any one vendor:

"If you find vulnerabilities in agents that everyone uses, like Codex and Claude Code, you have a new class of exploit. The labs are doing a lot of work here, but when a new platform emerges, a separate security system often emerges alongside it."

— Zico Kolter, Member, OpenAI Board of Directors, Safety & Security Committee; Professor, Carnegie Mellon University · Red-Teaming after Mythos, Latent Space

The mechanism behind that risk isn't mysterious. Simon Willison — who's spent the last two years cataloguing exactly this class of failure across the industry — put his finger on the root cause plainly:

"The key problem here is that LLMs are gullible. They believe anything that you tell them, but they believe anything that anyone else tells them as well. This is both a strength and a weakness. We want them to believe the stuff that we tell them, but if we think that we can trust them to make decisions based on unverified information they've been passed, we're going to end up in a lot of trouble."

— Simon Willison, Independent open source developer; creator of Datasette and the llm command-line tool; co-creator of Django · annotated transcript, AI Engineer World's Fair 2024

A model that believes whatever it's told is also a model that believes a working demo means the feature is done. The webhook fired. The task list rendered. The image uploaded. Every signal the agent had access to said success — because none of those signals include "was this request actually authenticated," which is invisible until someone goes looking for it on purpose.

What to check every time, regardless of which agent wrote the code

Here's the practical version, distilled from what actually broke above, not a generic checklist bolted on afterward:

  • Every webhook handler — does it call the provider's own signature-verification function (stripe.webhooks.constructEvent(), Razorpay's validateWebhookSignature(), or the equivalent) against the raw request body, not a parsed one?
  • Every mutating API route (POST/PATCH/PUT/DELETE) — does it derive the acting user from a verified session, or does it trust an id the caller supplied? A page being gated says nothing about the API routes underneath it.
  • Every credential — is it read from an environment variable with no hardcoded fallback anywhere in the file, including "just for local dev" constants?
  • Every route with no visible auth check — is that intentional (a public webhook, a public read endpoint) or an omission nobody caught because the demo worked anyway?

None of these require reading every line Claude Code writes, which doesn't scale past a feature or two a day. What scales is a deterministic gate that runs the same four checks — and more — on every generation, pass or fail, regardless of how confident the demo looked five minutes ago.

Run it yourself:

npx @agenticcli/shipguard scan --json

That's the exact free-tier command behind every result in this piece — no account, no code leaving your machine. For a CI or pre-deploy gate that actually blocks a bad merge instead of just reporting one, add --strict:

npx @agenticcli/shipguard scan --changed --strict --json

See what ShipGuard actually checks for the full rule set, including the categories that live beyond the free tier.

A clean scan isn't proof of security — it's proof that the specific patterns a scanner knows to look for aren't present. It's one gate cleared, not a verdict. But one clean generation out of four in this piece, run with zero adversarial intent, is exactly why that gate needs to run on all four, not just the ones that feel risky. Nothing about the demo told us which one was which. The scan did.

For the broader checklist this article's "check every time" section is drawn from, see vibe coding security: 12 checks before you ship. For how a general-purpose secret scanner stacks up against dedicated CLI tools on the credential-hygiene half of this problem, see GitHub secret scanner vs. CLI tools.


Published by AgenticCLI — developer tools for teams shipping AI-assisted code. ShipGuard is a CLI-first release gate that runs locally, with no code leaving your machine, and gates deploys via --strict in CI. See all checks at agenticcli.dev/shipguard.

FAQ

Is Claude Code safe to use?
As a tool, yes — Anthropic has published a real security model for it: read-only permissions by default, explicit approval required before edits or commands run, a sandboxed bash tool with filesystem and network isolation, and defenses against prompt injection. That's the harness. It's a separate question from whether the feature Claude Code just wrote for you is secure, because the harness can't grade the security properties of the code it produces — that's the gap this article is about.
Does Claude Code check its own code for security bugs?
Not by default, and Anthropic's own docs say so directly under 'user responsibility': you're responsible for reviewing proposed code and commands for safety before approval. There's an optional security-guidance plugin that has Claude review and fix vulnerabilities in its own changes during a session, which is a real step forward — but it's opt-in, and it's the same model grading its own homework rather than an independent, deterministic check.
What's the difference between Claude Code's permission system and a security scanner?
The permission system controls what Claude Code is allowed to touch on your machine — which files, which commands, which network calls. A scanner looks at what it produced after the fact and checks the code itself for known-bad patterns: a webhook with no signature check, a route with no auth guard, a hardcoded credential. One protects your system from the agent; the other checks the agent's output for the vulnerabilities it might have shipped. You need both, and they don't overlap.
What security mistakes does AI-generated code make most often?
In the four representative generations we scanned for this piece, the two real failures were a Stripe webhook handler that never verified the signature header (meaning anyone could POST a fake event) and a CRUD API that trusted a client-supplied user id instead of a verified session on every route. A third generation shipped a hardcoded AWS credential fallback. None of this required an unusual or adversarial prompt — these were first-draft, ordinary feature requests.
Should I manually review every line Claude Code writes?
Reading every line doesn't scale once you're shipping features daily, and it's exactly the failure mode a deterministic check exists to close — a scan takes seconds and doesn't get tired on the fortieth pull request. The realistic middle ground is: read the code you don't understand, and run an automated gate on every generation regardless of how confident it looks. A clean demo is not the same claim as a clean scan.
Can I trust a clean scan result as proof the code is secure?
No — a scan proves the absence of the specific patterns it checks for, not the presence of security. ShipGuard's free tier covers five categories (secrets, auth, database, payments, deployment); a clean result there says nothing about business logic flaws, authorization edge cases the pattern doesn't match, or categories the tool doesn't check yet. Treat a clean scan as one gate cleared, not a verdict.

────────[ ▮ gate ]────────

Don't ship the next one.

Free, local, no account. Catches this exact bug class before deploy.

$ npx @agenticcli/shipguard scan