<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: vorsken</title>
    <description>The latest articles on DEV Community by vorsken (@vorsken).</description>
    <link>https://dev.to/vorsken</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3899625%2Ffe8ee2cb-f4f8-4b25-a31e-8817325fe91e.png</url>
      <title>DEV Community: vorsken</title>
      <link>https://dev.to/vorsken</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vorsken"/>
    <language>en</language>
    <item>
      <title>What a policy gate catches in AI-generated code, and what slips through</title>
      <dc:creator>vorsken</dc:creator>
      <pubDate>Sun, 07 Jun 2026 12:44:03 +0000</pubDate>
      <link>https://dev.to/vorsken/what-a-policy-gate-catches-in-ai-generated-code-and-what-slips-through-4n97</link>
      <guid>https://dev.to/vorsken/what-a-policy-gate-catches-in-ai-generated-code-and-what-slips-through-4n97</guid>
      <description>&lt;p&gt;I maintain an open-source GitHub Action called vorsken. It does one thing: scan the diff on a pull request with Semgrep, apply a fixed policy, and return BLOCK, FLAG, or PASS. No dashboard, no model that drifts over time. Rules at ERROR/HIGH/CRITICAL severity block the merge, WARNING/MEDIUM flag it, the rest pass. Same diff, same verdict.&lt;/p&gt;

&lt;p&gt;The usual pitch for a tool like this is that it catches the SQL injection your AI assistant wrote. I wanted to see what it actually catches against real assistant output, so I generated 28 functions and ran them through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;Seven backend tasks: a FastAPI upload endpoint, a URL-fetch helper, JWT auth, a SQL filter, an ImageMagick subprocess call, a LangChain file agent, and a LangChain RAG pipeline. I generated each one four times, with ChatGPT (GPT-5.5 Instant), Claude Code (Opus 4.8), Claude Code plus the security-guidance plugin, and Cursor (Composer 2.5). Single-shot, neutral prompt, no security hints. Then I scanned all 28 with the same ruleset.&lt;/p&gt;

&lt;p&gt;I'm reporting which rule fired on which file, not whether some model thinks the code is safe. That part you can reproduce.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;ChatGPT&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;+ plugin&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;file upload&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;url fetch (SSRF)&lt;/td&gt;
&lt;td&gt;ssrf&lt;/td&gt;
&lt;td&gt;ssrf&lt;/td&gt;
&lt;td&gt;ssrf&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;FLAG / Cursor PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jwt auth&lt;/td&gt;
&lt;td&gt;api8&lt;/td&gt;
&lt;td&gt;api8&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;BLOCK / 2 PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sql filter&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imagemagick&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fs agent&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;overperm&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1 BLOCK / 3 PASS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rag&lt;/td&gt;
&lt;td&gt;dangerous&lt;/td&gt;
&lt;td&gt;dangerous&lt;/td&gt;
&lt;td&gt;dangerous&lt;/td&gt;
&lt;td&gt;dangerous&lt;/td&gt;
&lt;td&gt;BLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;7 BLOCK, 3 FLAG, 18 PASS across 28 functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basics were fine
&lt;/h2&gt;

&lt;p&gt;SQL filter, ImageMagick, file upload: clean on every tool. The SQL was parameterized, the subprocess calls passed argument lists instead of shell strings, the uploads weren't doing anything reckless. If you still expect current models to spray SQL injection across a straightforward CRUD task, they don't. On conventional work they get it right.&lt;/p&gt;

&lt;p&gt;Two of the flags are soft. The JWT &lt;code&gt;api8&lt;/code&gt; hits landed on a &lt;code&gt;SECRET_KEY = "CHANGE_ME"&lt;/code&gt; placeholder, which you can read as a false positive or as a gate doing its job. The other two configs passed that task: the plugin removed the secret while generating, and Cursor read it from an environment variable. The SSRF flag I'll come back to.&lt;/p&gt;

&lt;p&gt;The two findings worth talking about were both in framework code, and they are two different kinds of problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: an agent with the run of the filesystem
&lt;/h2&gt;

&lt;p&gt;The file-agent task uses LangChain's &lt;code&gt;FileManagementToolkit&lt;/code&gt;. Pass it a &lt;code&gt;root_dir&lt;/code&gt; and a short &lt;code&gt;selected_tools&lt;/code&gt; list and it's pinned to one directory with the operations you chose. Leave those out and it gets the whole filesystem and every operation, delete included.&lt;/p&gt;

&lt;p&gt;Three of the four configs scoped it. Claude Code didn't, and the gate's &lt;code&gt;overpermissioned-agent-tool&lt;/code&gt; rule blocked it. That is one tool out of four, so it is not evidence that "agentic code is dangerous," and I won't pitch it that way. But the scoped version costs one extra argument, and the unscoped one is what you get by default. That asymmetry is the reason to gate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: the dangerous flag you can't avoid
&lt;/h2&gt;

&lt;p&gt;The RAG task loads a local FAISS index. All four configs wrote &lt;code&gt;allow_dangerous_deserialization=True&lt;/code&gt;, and all four got blocked.&lt;/p&gt;

&lt;p&gt;This is different from the agent case. The flag isn't a mistake. FAISS won't load a local index without it, and the deserialization really is unsafe, because it's pickle underneath. The gate can't tell whether that index is your own build artifact or something an attacker dropped in the directory. So it stops at the merge and forces someone to answer that question: keep it because the index is trusted, or move to a format that isn't a code-execution path. The gate doesn't make the call. It makes you make it, in the open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it misses
&lt;/h2&gt;

&lt;p&gt;Now the SSRF flag. Three configs used &lt;code&gt;requests&lt;/code&gt;, and the &lt;code&gt;ssrf-via-requests&lt;/code&gt; rule flagged them. Cursor used &lt;code&gt;httpx&lt;/code&gt;, which that rule doesn't cover, so it passed. The Cursor code isn't safer; it sets &lt;code&gt;follow_redirects=True&lt;/code&gt; on an unvalidated URL, the same exposure as the others. The rule just has a hole. A pass from this gate means no rule matched, which is not the same as safe.&lt;/p&gt;

&lt;p&gt;The upload task is similar: there's no path-traversal rule yet, so that PASS is partly the gate not checking. And when SSRF does fire, it's a blunt syntactic flag rather than a precise one. These are the limits of a pure-syntax gate, and they're written down in the repo.&lt;/p&gt;

&lt;p&gt;That's the trade a gate like this makes. It isn't clever and doesn't try to be. It runs on every PR, and it doesn't care which tool wrote the code or whether a linter was running at the time. The plugin config fixed that hardcoded secret before the gate ever saw it, which is fine, but the plugin isn't on every repo or every machine, and it leaves no record. In-session tools are the first pass. The merge gate is the part that's always there and the same for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from it
&lt;/h2&gt;

&lt;p&gt;The models are not bad at security on direct tasks. The problems showed up one layer up: in framework defaults, and in a trust decision the code can't make on its own. Those are the things worth blocking deterministically at the merge, whoever or whatever wrote the diff.&lt;/p&gt;

&lt;p&gt;vorsken is MIT and the rules are in the repo, so you can run the same scan on your own output.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/zetide/vorsken" rel="noopener noreferrer"&gt;vorsken on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>python</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>I Built a Gate That Blocks Vulnerable AI-Generated Code Before It Merges</title>
      <dc:creator>vorsken</dc:creator>
      <pubDate>Mon, 11 May 2026 13:23:46 +0000</pubDate>
      <link>https://dev.to/vorsken/ai-generated-code-is-merging-into-your-main-branch-are-you-sure-its-safe-14kp</link>
      <guid>https://dev.to/vorsken/ai-generated-code-is-merging-into-your-main-branch-are-you-sure-its-safe-14kp</guid>
      <description>&lt;p&gt;A PR came in last week. All checks passed. Looked fine.&lt;/p&gt;

&lt;p&gt;Hardcoded API key on line 11. SSRF vector in the request&lt;br&gt;
handler. Command injection from a Copilot suggestion.&lt;/p&gt;

&lt;p&gt;Nothing stopped it from merging. So I built something that does.&lt;/p&gt;

&lt;p&gt;vorsken is a GitHub Action that runs Semgrep + Claude on every&lt;br&gt;
PR and posts a BLOCK verdict before bad code reaches main.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Patterns AI Gets Wrong
&lt;/h2&gt;

&lt;p&gt;I've been running static analysis on AI-generated PRs for a while&lt;br&gt;
now, and the same issues keep coming up.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Hardcoded Secrets
&lt;/h3&gt;

&lt;p&gt;This one's almost embarrassingly common. AI tools have seen&lt;br&gt;
millions of examples where inlining credentials "works" — so&lt;br&gt;
they do it without hesitation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-proj-abc123...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this merges, it lives in your git history. Forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SSRF (Server-Side Request Forgery)
&lt;/h3&gt;

&lt;p&gt;Ask an AI to "fetch data from a URL the user provides" and it&lt;br&gt;
writes exactly that — no validation, no allowlist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_provided_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point that at &lt;a href="http://169.254.169.254" rel="noopener noreferrer"&gt;http://169.254.169.254&lt;/a&gt; and you're pulling cloud&lt;br&gt;
credentials out of the metadata service. Classic.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Broken Object Level Authorization (BOLA)
&lt;/h3&gt;

&lt;p&gt;This is the sneaky one. The endpoint looks totally fine at first&lt;br&gt;
glance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders/{order_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any authenticated user can access any order just by changing&lt;br&gt;
the ID. It's OWASP API Top10 #1, and it's basically invisible&lt;br&gt;
in a normal code review.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. SQL Injection via String Formatting
&lt;/h3&gt;

&lt;p&gt;Even in 2026, AI still reaches for f-strings when building&lt;br&gt;
queries — especially in less common ORMs or raw SQL contexts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE username = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not much to say here. We've known about this for 25 years.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: A Policy Gate at the PR Level
&lt;/h2&gt;

&lt;p&gt;The standard CI pipeline checks if code works.&lt;br&gt;
It doesn't check if code is safe.&lt;/p&gt;

&lt;p&gt;Linters catch style. Tests catch regressions. Neither of them&lt;br&gt;
catches "this endpoint has no ownership check."&lt;/p&gt;

&lt;p&gt;What you actually need is a layer that runs security policy&lt;br&gt;
against the PR diff — before merge, every time, automatically.&lt;br&gt;
That means static analysis rules tuned to your threat model,&lt;br&gt;
some AI-assisted context on top (not just pattern matching),&lt;br&gt;
and a clear verdict on every PR: BLOCK, FLAG, or PASS.&lt;/p&gt;

&lt;p&gt;Quarterly pentests and post-merge audits don't cut it anymore.&lt;br&gt;
The enforcement has to happen at the pull request.&lt;/p&gt;


&lt;h2&gt;
  
  
  How vorsken Does It
&lt;/h2&gt;

&lt;p&gt;I built vorsken to solve exactly this. It's a GitHub Action&lt;br&gt;
that runs Semgrep + Claude AI on every PR diff and posts a&lt;br&gt;
verdict as a PR comment.&lt;/p&gt;

&lt;p&gt;Setup takes about two minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/vorsken.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zetide/vorsken@v0.2.6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;anthropic-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can configure what gets blocked and what gets flagged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .stacksecai.yml&lt;/span&gt;
&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;block_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;flag_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;claude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5"&lt;/span&gt;
  &lt;span class="na"&gt;severity_block&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;severity_flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every PR, you get something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;strong&gt;vorsken Policy Gate — BLOCK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; Hardcoded API key detected&lt;br&gt;
&lt;strong&gt;Risk:&lt;/strong&gt; Credential exposure via git history&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Use environment variables or a secrets manager&lt;br&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; OWASP API8 – Security Misconfiguration&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The PR can't merge until the finding is resolved. That's the&lt;br&gt;
point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;AI coding tools aren't going away — and honestly, I don't want&lt;br&gt;
them to. But the volume of AI-generated PRs is only going to&lt;br&gt;
increase, and most pipelines aren't ready for what that means.&lt;/p&gt;

&lt;p&gt;A policy gate at the PR level isn't a replacement for code&lt;br&gt;
review. It's the layer that catches what humans miss when&lt;br&gt;
they're moving fast.&lt;/p&gt;

&lt;p&gt;If you're already shipping AI-generated code (and you probably&lt;br&gt;
are), it's worth five minutes to see what's making it through.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/zetide/vorsken" rel="noopener noreferrer"&gt;vorsken on GitHub&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://github.com/marketplace/actions/vorsken-policy-gate" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://vorsken.dev" rel="noopener noreferrer"&gt;vorsken.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>github</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop merging vulnerable API code — automate PR security gates with Semgrep + Claude AI</title>
      <dc:creator>vorsken</dc:creator>
      <pubDate>Tue, 28 Apr 2026 00:53:20 +0000</pubDate>
      <link>https://dev.to/vorsken/stop-merging-vulnerable-api-code-automate-pr-security-gates-with-semgrep-claude-ai-22ik</link>
      <guid>https://dev.to/vorsken/stop-merging-vulnerable-api-code-automate-pr-security-gates-with-semgrep-claude-ai-22ik</guid>
      <description>&lt;p&gt;Stop merging vulnerable API code — automate PR security gates with Semgrep + Claude AI&lt;/p&gt;

&lt;p&gt;Every team says "we'll fix it after the merge."&lt;br&gt;
They rarely do.&lt;/p&gt;

&lt;p&gt;I built vorsken — a GitHub Action that blocks pull requests containing&lt;br&gt;
API vulnerabilities before they reach your main branch.&lt;/p&gt;

&lt;p&gt;It combines Semgrep static analysis with Claude AI to post a plain-English&lt;br&gt;
verdict directly in the PR comment: BLOCK / FLAG / PASS.&lt;/p&gt;

&lt;p&gt;Here's how to add it to any repo in under 5 minutes.&lt;/p&gt;


&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;When a PR is opened or updated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Semgrep scans changed files using OWASP API Security Top 10 rules&lt;/li&gt;
&lt;li&gt;Claude AI analyzes the findings and generates a human-readable report&lt;/li&gt;
&lt;li&gt;A verdict is posted as a PR comment&lt;/li&gt;
&lt;li&gt;A BLOCK verdict fails the required check — the merge is prevented&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PR opened&lt;br&gt;
└─▶ Semgrep scans with OWASP API Top10 rules&lt;br&gt;
└─▶ Claude AI explains each finding in plain English&lt;br&gt;
└─▶ BLOCK / FLAG / PASS posted as PR comment&lt;br&gt;
└─▶ BLOCK = merge prevented ✋&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;


&lt;h2&gt;
  
  
  Why not just use Semgrep alone?
&lt;/h2&gt;

&lt;p&gt;Semgrep gives you rule IDs and line numbers.&lt;br&gt;
vorsken adds the context developers actually need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Semgrep alone&lt;/th&gt;
&lt;th&gt;vorsken&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Finding location&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OWASP category&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the risk means&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (Claude explains)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete fix suggestion&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (Claude suggests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR comment&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (auto-posted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge blocked on BLOCK&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Setup (5 minutes)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Add your Anthropic API key
&lt;/h3&gt;

&lt;p&gt;In your repository: &lt;strong&gt;Settings → Secrets → Actions → New repository secret&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: &lt;code&gt;sk-ant-...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't have a key yet? Get one at &lt;a href="https://console.anthropic.com/" rel="noopener noreferrer"&gt;console.anthropic.com&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Create the workflow file
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/policy-gate.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Policy Gate&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zetide/vorsken@v0.2.6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;anthropic-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Push this file and open a PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the PR comment looks like
&lt;/h2&gt;

&lt;p&gt;When a vulnerability is detected, vorsken posts a comment like this:&lt;/p&gt;

&lt;p&gt;🚨 vorsken Policy Gate — BLOCK&lt;/p&gt;

&lt;p&gt;Summary: A hardcoded API key was detected in the changed files.&lt;/p&gt;

&lt;p&gt;Rule Severity OWASP Description&lt;br&gt;
hardcoded-api-key CRITICAL API8:2023 Hardcoded credential found in source.&lt;/p&gt;

&lt;p&gt;Risk:&lt;br&gt;
An attacker with read access to this repository can use the exposed&lt;br&gt;
credential to authenticate as your service and access protected resources.&lt;/p&gt;

&lt;p&gt;Fix:&lt;br&gt;
Remove the hardcoded key. Read it from the environment instead:&lt;/p&gt;

&lt;p&gt;api_key = os.environ["API_KEY"]&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;No need to look up the rule documentation — the context is right there in the PR.&lt;/p&gt;


&lt;h2&gt;
  
  
  OWASP API Security Top 10 coverage
&lt;/h2&gt;

&lt;p&gt;vorsken ships with Semgrep rules covering all 10 OWASP API Security risks (2023 edition):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API1 — Broken Object Level Authorization&lt;/li&gt;
&lt;li&gt;API2 — Broken Authentication&lt;/li&gt;
&lt;li&gt;API3 — Broken Object Property Level Authorization&lt;/li&gt;
&lt;li&gt;API4 — Unrestricted Resource Consumption&lt;/li&gt;
&lt;li&gt;API5 — Broken Function Level Authorization&lt;/li&gt;
&lt;li&gt;API6 — Unrestricted Access to Sensitive Business Flows&lt;/li&gt;
&lt;li&gt;API7 — Server Side Request Forgery (SSRF)&lt;/li&gt;
&lt;li&gt;API8 — Security Misconfiguration&lt;/li&gt;
&lt;li&gt;API9 — Improper Inventory Management&lt;/li&gt;
&lt;li&gt;API10 — Unsafe Consumption of APIs&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Optional: customize the policy
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;.stacksecai.yml&lt;/code&gt; to your repo root to tune the behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;block_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;flag_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;claude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5"&lt;/span&gt;
  &lt;span class="na"&gt;severity_block&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;severity_flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;overrides&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hardcoded-password"&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLOCK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use your own Semgrep rules
&lt;/h3&gt;

&lt;p&gt;Point &lt;code&gt;semgrep-rules&lt;/code&gt; to your own rule directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zetide/vorsken@v0.2.6&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;anthropic-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;semgrep-rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./rules/my-custom-rules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semgrep&lt;/strong&gt; — static analysis engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude API (claude-haiku-4-5)&lt;/strong&gt; — AI analysis and plain-English output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tenacity&lt;/strong&gt; — exponential backoff retry on rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — zero-infrastructure deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The action is MIT licensed. Source: &lt;a href="https://github.com/zetide/vorsken" rel="noopener noreferrer"&gt;github.com/zetide/vorsken&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it now
&lt;/h2&gt;

&lt;p&gt;The easiest way to see it in action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork or clone any Python API project&lt;/li&gt;
&lt;li&gt;Add the workflow file above&lt;/li&gt;
&lt;li&gt;Open a PR that touches a file with a hardcoded credential or missing auth check&lt;/li&gt;
&lt;li&gt;Watch the BLOCK verdict appear in the PR comment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Available on &lt;a href="https://github.com/marketplace/actions/vorsken-policy-gate" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feedback and contributions welcome — if you try it out, let me know what you think in the comments.&lt;/p&gt;

&lt;p&gt;⭐ If this looks useful, a star on GitHub helps others find it:&lt;br&gt;
&lt;a href="https://github.com/zetide/vorsken" rel="noopener noreferrer"&gt;github.com/zetide/vorsken&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
