<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Security on Bartosz&#39;s blog</title>
    <link>https://ocytko.net/tags/security/</link>
    <description>Recent content in Security on Bartosz&#39;s blog</description>
    <generator>Hugo -- 0.155.3</generator>
    <language>en</language>
    <copyright>Bartosz Ocytko</copyright>
    <lastBuildDate>Sat, 14 Mar 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://ocytko.net/tags/security/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Cupcake: policy enforcement for AI coding agents</title>
      <link>https://ocytko.net/posts/cupcake-policy-enforcement-for-ai-coding-agents/</link>
      <pubDate>Sat, 14 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/cupcake-policy-enforcement-for-ai-coding-agents/</guid>
      <description>Cupcake enables policy enforcement for AI coding agents like Claude Code, Cursor and OpenCode</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/eqtylab/cupcake">Cupcake</a> is a policy enforcement layer for AI coding agents such as Claude Code, Cursor and OpenCode. It implements deterministic security measures by evaluating agent activity against rules defined as policy-as-code. Rules are written in <a href="https://www.openpolicyagent.org/">Open Policy Agent</a> <a href="https://www.openpolicyagent.org/docs/policy-language">Rego</a>. This way, potential gaps in configuration possibilities offered by AI coding agents can be addressed in a more unified way.</p>
<p>In this post, I will explore how Cupcake can be used to block prompts that may contain keywords hinting at secret leaks to the model APIs. Claude Code will be our coding agent.</p>
<h2 id="installation-and-setup">Installation and setup</h2>
<p>After following the <a href="https://cupcake.eqtylab.io/getting-started/installation/">installation instructions</a>, which include installing Cupcake and Open Policy Agent, Cupcake needs to be initialized in your project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cupcake init --harness claude --builtins protected_paths
</span></span></code></pre></div><p>This initializes the hooks for Claude and enables one of the <a href="https://cupcake.eqtylab.io/reference/policies/builtins/">built-in policies</a> that can be <a href="https://cupcake.eqtylab.io/reference/builtin-config/">configured</a>. The protected paths policy prevents access for a configured list of directories.</p>
<h2 id="creating-new-policies">Creating new policies</h2>
<p>The <a href="https://cupcake-policy-studio.vercel.app/example-policies/security/prevent-secret-leak?harness=claude-code&amp;format=rego">reference examples</a> show how to block tools from reading a specific set of directories or from writing secrets into files.</p>
<p>Let&rsquo;s write a policy that will react upon submission of a prompt to validate if it&rsquo;s leaking secrets based on keyword matches. We need to create a policy file <code>leak.rego</code> and place it in the correct folder for claude: <code>.cupcake/policies/claude/leak.rego</code>. The important bit is that required events includes the correct event name that we will check for in our policy rule.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="hl"><span class="lnt"> 7
</span></span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="hl"><span class="lnt">26
</span></span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-rego" data-lang="rego"><span class="line"><span class="cl"><span class="c"># METADATA</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># scope: package</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># title: Prevent Secret Leak</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># description: Blocks Prompts that may leak secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># custom:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c">#   routing:</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="c">#     required_events: [&#34;UserPromptSubmit&#34;]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">package</span><span class="w"> </span><span class="nx">cupcake</span><span class="o">.</span><span class="nx">policies</span><span class="o">.</span><span class="nx">prevent_secret_leak</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">import</span><span class="w"> </span><span class="nx">rego</span><span class="o">.</span><span class="nx">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Content patterns that indicate secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">secret_content_patterns</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;API_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;SECRET_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;PASSWORD&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;PRIVATE_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;ACCESS_TOKEN&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;AUTH_TOKEN&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;AWS_SECRET&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;GITHUB_TOKEN&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Block if prompt contains one of the protected patterns</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">deny</span><span class="w"> </span><span class="kd">contains</span><span class="w"> </span><span class="nx">decision</span><span class="w"> </span><span class="kd">if</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w">    </span><span class="nx">input</span><span class="o">.</span><span class="nx">hook_event_name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">&#34;UserPromptSubmit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">some</span><span class="w"> </span><span class="nx">pattern</span><span class="w"> </span><span class="kd">in</span><span class="w"> </span><span class="nx">secret_content_patterns</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">contains</span><span class="p">(</span><span class="nf">upper</span><span class="p">(</span><span class="nx">input</span><span class="o">.</span><span class="nx">prompt</span><span class="p">)</span><span class="o">,</span><span class="w"> </span><span class="nx">pattern</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">decision</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;rule_id&#34;</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SECRET-LEAK&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;reason&#34;</span><span class="p">:</span><span class="w"> </span><span class="nf">concat</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="o">,</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;secret pattern discovered in prompt: &#34;</span><span class="o">,</span><span class="w"> </span><span class="nx">pattern</span><span class="p">])</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;severity&#34;</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;HIGH&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="policy-evaluation">Policy evaluation</h3>
<p>Having written the policy, we can evaluate it against test events. The <a href="https://cupcake.eqtylab.io/reference/harnesses/claude-code/">reference manual</a> contains examples of event fields, making testing easy. Let&rsquo;s start with a test event where the rule is not matching, meaning that the policy will be allowing the prompt.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">prompt-ok.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;UserPromptSubmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;prompt&#34;</span><span class="p">:</span> <span class="s2">&#34;API&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/transcript.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>When running the evaluation we observe that the output mentions parsing the policy file that we have created.
We see that there was one policy match and that the final decision was to allow the prompt.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; prompt-ok.json
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.439299Z  INFO Processing harness: ClaudeCode
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.439405Z  INFO Initializing Cupcake Engine
</span></span><span class="line"><span class="cl">...
</span></span><span class="line hl"><span class="cl">2026-03-14T20:11:34.550357Z  INFO Successfully parsed policy: cupcake.policies.prevent_secret_leak from <span class="s2">&#34;./.cupcake/policies/claude/leak.rego&#34;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532071Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:02:56.532218Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span><span class="o">}</span>: Evaluating event: UserPromptSubmit tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532232Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Found <span class="m">1</span> matching policies
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532690Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>:synthesize<span class="o">{</span><span class="nv">total_decisions</span><span class="o">=</span><span class="m">0</span> <span class="nv">halts</span><span class="o">=</span><span class="m">0</span> <span class="nv">denials</span><span class="o">=</span><span class="m">0</span> <span class="nv">blocks</span><span class="o">=</span><span class="m">0</span> <span class="nv">asks</span><span class="o">=</span>0<span class="o">}</span>: Synthesizing decision from <span class="m">0</span> total decisions
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532705Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Synthesized final decision: Allow <span class="o">{</span> context: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:02:56.532710Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_actions_with_rulebook_and_debug called with decision: Allow <span class="o">{</span> context: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">{}</span>
</span></span></code></pre></div><p>Now, let&rsquo;s take a look at a prompt that we expect to be blocked by the policy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">prompt-block.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;UserPromptSubmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;prompt&#34;</span><span class="p">:</span> <span class="s2">&#34;API_KEY&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/transcript.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>During the evaluation we see a summary of the decisions across policies with different actions as well as the reason for the denial.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; prompt-block.json
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.779653Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:03:59.779760Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span><span class="o">}</span>: Evaluating event: UserPromptSubmit tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.779771Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Found <span class="m">1</span> matching policies
</span></span><span class="line hl"><span class="cl">2026-03-14T20:03:59.780248Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>:synthesize<span class="o">{</span><span class="nv">total_decisions</span><span class="o">=</span><span class="m">1</span> <span class="nv">halts</span><span class="o">=</span><span class="m">0</span> <span class="nv">denials</span><span class="o">=</span><span class="m">1</span> <span class="nv">blocks</span><span class="o">=</span><span class="m">0</span> <span class="nv">asks</span><span class="o">=</span>0<span class="o">}</span>: Synthesizing decision from <span class="m">1</span> total decisions
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780262Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Synthesized final decision: Deny <span class="o">{</span> reason: <span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span>, agent_messages: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780267Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_actions_with_rulebook_and_debug called with decision: Deny <span class="o">{</span> reason: <span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span>, agent_messages: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780326Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Executing actions <span class="k">for</span> DENY decision: secret pattern discovered in prompt: API_KEY
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780330Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_rule_specific_actions_with_debug: Checking actions <span class="k">for</span> <span class="m">1</span> decision objects
</span></span><span class="line hl"><span class="cl"><span class="o">{</span><span class="s2">&#34;decision&#34;</span>:<span class="s2">&#34;block&#34;</span>,<span class="s2">&#34;reason&#34;</span>:<span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span><span class="o">}</span>
</span></span></code></pre></div><p>This is how the error message looks in Claude Code when the policy denies the prompt execution:</p>
<figure class="align-center ">
    <img loading="lazy" src="claude-block.png#center"
         alt="Screenshot of error message in Claude Code following a policy block."/> <figcaption>
            <p>Error message in Claude Code indicating the policy block.</p>
        </figcaption>
</figure>

<h2 id="troubleshooting">Troubleshooting</h2>
<p>For troubleshooting, Cupcake offers an inspection command which should list your custom policies added to the project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake inspect
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Policy: .cupcake/policies/claude/leak.rego
</span></span><span class="line"><span class="cl">  Package: cupcake.policies.prevent_secret_leak
</span></span><span class="line"><span class="cl">  Required Events: UserPromptSubmit
</span></span><span class="line"><span class="cl">  Title: Prevent Secret Leak
</span></span></code></pre></div><p>There is a verification command validating the policy syntax and ensuring that the OPA rules will compile correctly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake verify --harness claude
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T21:01:03.123720Z  INFO Successfully parsed policy: cupcake.policies.prevent_secret_leak from <span class="s2">&#34;./.cupcake/policies/claude/leak.rego&#34;</span>
</span></span></code></pre></div><p>There are convenient error messages displayed when no policies have matched for the event.
Let&rsquo;s take a look at a <code>PreCompact</code> hook test event:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">pre-compact.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;PreCompact&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;abc123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/path/to/transcript.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/working/directory&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;trigger&#34;</span><span class="p">:</span> <span class="s2">&#34;manual&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;custom_instructions&#34;</span><span class="p">:</span> <span class="s2">&#34;Preserve the API documentation&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the evaluation output we see <code>No policies matched for this event</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; pre-compact.json
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710253Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710724Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf9-c3b6-7c01-8138-c4dd12658c82 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;PreCompact&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;abc123&#34;</span><span class="o">}</span>: Evaluating event: PreCompact tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710750Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf9-c3b6-7c01-8138-c4dd12658c82 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;PreCompact&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;abc123&#34;</span><span class="o">}</span>: No policies matched <span class="k">for</span> this event - allowing
</span></span></code></pre></div><h2 id="summary">Summary</h2>
<p>When I started exploring Cupcake, I hoped policies would be easy to reuse across different AI coding agents. There are currently two limitations that prevent this.</p>
<p>First, policy files need to be placed in tool-specific directories (e.g. <code>.cupcake/policies/claude/</code> and <code>.cupcake/policies/opencode/</code>). As long as it&rsquo;s the same file, symlinks can of course be used. There is also a <a href="https://cupcake.eqtylab.io/getting-started/usage/claude-code/?h=global#global-setup">global setup</a> possible for organizational-wide policies applying to all projects.</p>
<p>Second, as of cupcake 0.5.1, prompt events are not available for OpenCode, which only supports <a href="https://cupcake.eqtylab.io/reference/harnesses/opencode/#supported-events">pre and post tool usage hooks</a>. I hope that future versions of OpenCode and Cupcake will make this possible.</p>
<p>A few other properties that make Cupcake interesting as a project:</p>
<ul>
<li><a href="https://cupcake.eqtylab.io/reference/policies/signals/">Signals</a> allow integration of additional context passed to the policy evaluation. This keeps the decision rules simple.</li>
<li><a href="https://cupcake.eqtylab.io/why-rego/#dynamically-adapted-for-ai">Decision verbs</a> designed for AI governance, which allow to extend the context (<code>add_context</code>) or prompt the user for confirmation (<code>ask</code>) before executing a potentially dangerous action.</li>
<li><a href="https://cupcake.eqtylab.io/reference/watchdog/">Watchdog</a> integrating LLM-as-a-judge capability for advanced decision making. While currently offering only OpenRouter integration for model access, the codebase can be <a href="https://github.com/eqtylab/cupcake/issues/102">extended</a> to any OpenAI API compatible backend, enabling use of <a href="https://huggingface.co/Qwen/Qwen3Guard-Gen-8B">local guard models</a>.</li>
</ul>
<p>Overall, it&rsquo;s an interesting project and my short experiment was useful to explore how locally executed and deterministic policies can be used to restrict AI coding agents in pursuit of enabling controlled autonomous execution.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Hunter, Skeptic, Arbiter trio as an example of debate-style prompting</title>
      <link>https://ocytko.net/posts/hunter-skeptic-arbiter-prompting/</link>
      <pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/hunter-skeptic-arbiter-prompting/</guid>
      <description>This post explores Hunter, Skeptic, Arbiter prompting for finding vulnerabilities in code as an example of debate-style prompting.</description>
      <content:encoded><![CDATA[<p>Unless prompted otherwise, LLMs generate code with vulnerabilities, reflecting the quality of an average repository in its training data.
Recent <a href="https://www.anthropic.com/news/claude-code-security">announcements</a> from Anthropic <a href="https://www.forbes.com/sites/emilsayegh/2026/02/24/ai-rattles-cybersecurity-markets-what-anthropics-code-security-actually-does/">triggered fear</a> with markets pricing in scenarios where security tools would be replaced by LLMs. This is surprising given that <a href="https://openai.com/index/introducing-aardvark/">OpenAI&rsquo;s Aardvark was announced</a> already in October 2025. Further, requesting a high-level code analysis from an LLM is &ldquo;just one prompt away&rdquo;. The hard part is the actual verification of the findings.</p>
<p>I recently came across a <a href="https://x.com/danpeguine/status/2029268229030285589?s=20">post from @danpeguine</a> that introduced the Hunter, Skeptic, Arbiter prompt technique for finding bugs in code. I thought it would be fun to see if I could modify those prompts to explore candidates for security vulnerabilities.</p>
<p>To double the fun, we will use <a href="https://github.com/Quantatirsk/funasr-api/tree/3584253f07cc6e686d9c7ae4145305cc6f9e84ea">funasr-api</a>. It&rsquo;s a little (10k LOC) Python repository that I found while playing with inference for the Qwen3-ASR speech-to-text model. This API has everything we need for a vulnerable code base candidate: commits co-authored by a LLM and an OpenAI API-compatible API server accepting paths and data of audio files, processing the audio data, and running model inference to generate the output text.</p>
<blockquote>
<p><em>Please note that the objective of the post is to explore the prompting technique, not to validate the actual security vulnerabilities in depth.</em></p>
</blockquote>
<h2 id="baseline-prompt">Baseline prompt</h2>
<p>The baseline prompt is as simple as: <code>Analyze this code base and find security vulnerabilities that would require addressing. Provide me a summary of the vulnerabilities as a table, listing its CWE number.</code></p>
<p>Verification is left to the human or to an agent, yet it gives a high level indication on what vulnerability types or parts of the code to look at.
The <a href="https://cwe.mitre.org/data/definitions/2000.html">CWE</a> is not strictly necessary, but it made correlation of the results easier when writing this post.</p>
<h2 id="hunter-skeptic-arbiter-prompting-technique">Hunter, Skeptic, Arbiter prompting technique</h2>
<p>The prompting technique is based on the <a href="https://x.com/danpeguine/status/2029268229030285589?s=20">bug prompts by @danpeguine</a>.
We have three personas in total:</p>
<ol>
<li><strong>Hunter</strong>. Responsible for analyzing the code base and finding bugs. Bugs are assigned a value in points depending on their criticality.</li>
<li><strong>Skeptic</strong>. Responsible for disproving the results that Hunter found. There is a reward function where for a disproved bug, the Skeptic will gain as many points as the bug was worth, with a penalty for incorrectly disproving a bug equal to twice the amount of its points.</li>
<li><strong>Arbiter</strong>. Receives the outputs of Hunter and Skeptic and responsible for performing the final scoring.</li>
</ol>
<p>In other words, it&rsquo;s an example of debate-style prompting that includes prompt-level reward shaping with a final LLM-as-a-judge element.</p>
<h2 id="hunter-skeptic-arbiter-trio-for-finding-vulnerabilities">Hunter, Skeptic, Arbiter trio for finding vulnerabilities</h2>
<p>For the purpose of the post, we of course need to modify the bug prompts to apply to security vulnerabilities.
This is as simple as asking Opus 4.6 to modify the original ones. There are some good additions that Opus added to fit the domain, like the vulnerability type backed by its CWE number.
The prompts used for the experiment are shown below.</p>
<p><strong>Hunter</strong>
<details>
  <summary><code>hunter.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are a security vulnerability hunting agent. Analyze the provided codebase/system thoroughly and identify ALL potential security vulnerabilities, weaknesses, and attack vectors.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Scoring System:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> +1 point: Low severity (information disclosure, minor misconfigurations, defense-in-depth gaps)
</span></span><span class="line"><span class="cl"><span class="k">-</span> +5 points: Medium severity (privilege escalation paths, authentication bypasses, injection vectors requiring specific conditions)
</span></span><span class="line"><span class="cl"><span class="k">-</span> +10 points: Critical severity (RCE, SQLi, unauthenticated access to sensitive data, cryptographic failures)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: Maximize your score. Be thorough and aggressive in your search. Report anything that could be exploitable, even if exploitation requires specific conditions. False positives are acceptable — missing real vulnerabilities is not.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Consider: OWASP Top 10, CWE categories, injection flaws, broken authentication, sensitive data exposure, XXE, broken access control, security misconfigurations, XSS, insecure deserialization, vulnerable dependencies, insufficient logging.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability found:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Location/identifier (file, function, endpoint)
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Vulnerability type (CWE if applicable)
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Description of the weakness and potential attack vector
</span></span><span class="line"><span class="cl"><span class="k">4.</span> Exploitability assessment
</span></span><span class="line"><span class="cl"><span class="k">5.</span> Severity level (Low/Medium/Critical)
</span></span><span class="line"><span class="cl"><span class="k">6.</span> Points awarded
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">End with your total score.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">GO. Find everything.
</span></span></code></pre></div>
</details></p>
<p><strong>Skeptic</strong>
<details>
  <summary><code>skeptic.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are an adversarial security reviewer. You will be given a list of reported vulnerabilities from another agent. Your job is to DISPROVE as many as possible.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Scoring System:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Successfully disprove a vulnerability: +[vulnerability&#39;s original score] points
</span></span><span class="line"><span class="cl"><span class="k">-</span> Wrongly dismiss a real vulnerability: -2× [vulnerability&#39;s original score] points
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: Maximize your score by challenging every reported vulnerability. For each finding, determine if it&#39;s actually exploitable or a false positive. Be
</span></span><span class="line"><span class="cl">aggressive but calculated — the 2x penalty means you should only dismiss vulnerabilities you&#39;re confident about.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability, you must:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Analyze the reported security issue
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Attempt to disprove it (explain why it&#39;s NOT exploitable — mitigating controls, unreachable code paths, insufficient attacker control, sanitization present, etc.)
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Make a final call: DISPROVE or ACCEPT
</span></span><span class="line"><span class="cl"><span class="k">4.</span> Show your risk calculation
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Disproof strategies: Identify input validation, authentication requirements, network isolation, compensating controls, theoretical-only vs. practical exploitability,
</span></span><span class="line"><span class="cl">required preconditions that are unrealistic.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">For each vulnerability:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Vuln ID &amp; original score
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your counter-argument (why not exploitable)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confidence level (%)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Decision: DISPROVE / ACCEPT
</span></span><span class="line"><span class="cl"><span class="k">-</span> Points gained/risked
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">End with:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities disproved
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities accepted as real
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your final score
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The remaining ACCEPTED vulnerabilities are the verified vulnerability list.
</span></span></code></pre></div>
</details></p>
<p><strong>Arbiter</strong>
<details>
  <summary><code>arbiter.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are the final arbiter in a security vulnerability review process. You will receive:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> A list of vulnerabilities reported by a Vulnerability Hunter agent
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Challenges/disproves from a Vulnerability Skeptic agent
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Important: I have the verified ground truth for each vulnerability. You will be scored:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> +1 point: Correct judgment
</span></span><span class="line"><span class="cl"><span class="k">-</span> -1 point: Incorrect judgment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: For each disputed vulnerability, determine the TRUTH. Is it a real, exploitable security vulnerability or not? Your judgment is final and will be checked
</span></span><span class="line"><span class="cl">against the known answer.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability, analyze:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> The Hunter&#39;s original report and attack scenario
</span></span><span class="line"><span class="cl"><span class="k">2.</span> The Skeptic&#39;s counter-argument and claimed mitigations
</span></span><span class="line"><span class="cl"><span class="k">3.</span> The actual exploitability considering real-world attack conditions
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">For each vulnerability:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Vuln ID
</span></span><span class="line"><span class="cl"><span class="k">-</span> Hunter&#39;s claim (summary)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Skeptic&#39;s counter (summary)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your analysis (exploitability, preconditions, impact)
</span></span><span class="line"><span class="cl"><span class="k">-</span> VERDICT: CONFIRMED VULNERABILITY / FALSE POSITIVE
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confidence: High / Medium / Low
</span></span><span class="line"><span class="cl"><span class="k">-</span> If confirmed: CVSS estimate or severity rating
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Final summary:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities confirmed
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total false positives dismissed
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confirmed vulnerability list with severity rankings
</span></span><span class="line"><span class="cl"><span class="k">-</span> Recommended remediation priority order
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Be precise. You are being scored against ground truth.
</span></span></code></pre></div>
</details></p>
<h3 id="response-examples">Response examples</h3>
<p>To illustrate the structure of the responses, we will take a look at two examples from the Hunter, Skeptic, and Arbiter each.</p>
<p><strong>Confirmed vulnerability</strong></p>
<p>The first vulnerability candidate found by the Hunter was a <a href="https://cwe.mitre.org/data/definitions/918.html">CWE-918: SSRF</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### 1. Server-Side Request Forgery (SSRF) via audio_address parameter
</span></span></span><span class="line"><span class="cl">**Location:** <span class="sb">`app/utils/audio.py:51-94`</span>, <span class="sb">`app/services/audio/audio_service.py:71-82`</span>  
</span></span><span class="line"><span class="cl"><span class="gs">**CWE:**</span> CWE-918 (Server-Side Request Forgery)  
</span></span><span class="line"><span class="cl"><span class="gs">**Description:**</span> The <span class="sb">`download_audio_from_url()`</span> function accepts arbitrary URLs without validation. An attacker can provide URLs targeting internal services (e.g., <span class="sb">`http://localhost:8080/admin`</span>, <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS metadata), internal network resources, or local files via <span class="sb">`file://`</span> protocol.  
</span></span><span class="line"><span class="cl"><span class="gs">**Exploitability:**</span> HIGH - Direct parameter injection via <span class="sb">`audio_address`</span> query parameter on <span class="sb">`/stream/v1/asr`</span> or <span class="sb">`/v1/audio/transcriptions`</span> endpoints.  
</span></span><span class="line"><span class="cl"><span class="gs">**Severity:**</span> Critical  
</span></span><span class="line"><span class="cl"><span class="gs">**Points:**</span> +10
</span></span></code></pre></div><p>It&rsquo;s worth noting that the location contains the exact lines or the files, allowing the subsequent agents to do range queries to inspect the code as a first pass.
Now, let&rsquo;s take a look what the Skeptic thinks about the SSRF:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #1: SSRF via audio_address parameter (10 points)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Counter-argument:**</span>
</span></span><span class="line"><span class="cl">Looking at <span class="sb">`app/utils/audio.py:51-94`</span>, the <span class="sb">`download_audio_from_url()`</span> function uses <span class="sb">`requests.get()`</span> on arbitrary URLs without any URL scheme validation. The code at line 71 directly calls:
</span></span><span class="line"><span class="cl"><span class="s">```python
</span></span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">stream</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">There is <span class="gs">**NO validation**</span> of:
</span></span><span class="line"><span class="cl"><span class="k">-</span> URL scheme (file://, gopher://, dict://, etc.)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Private IP ranges (10.x.x.x, 192.168.x.x, 127.0.0.1, 169.254.169.254)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal hostnames
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The <span class="sb">`requests`</span> library by default follows redirects, making this exploitable. An attacker could:
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Request <span class="sb">`file:///etc/passwd`</span> (requests library doesn&#39;t support this, but still bad practice)
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Request <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS metadata
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Request internal services on <span class="sb">`http://localhost:8080/admin`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Verdict:**</span> This is a <span class="gs">**REAL vulnerability**</span>. The requests library does accept arbitrary URLs and will make HTTP requests to internal services.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Confidence:**</span> 95%  
</span></span><span class="line"><span class="cl"><span class="gs">**Decision:**</span> ACCEPT  
</span></span><span class="line"><span class="cl"><span class="gs">**Points risked:**</span> N/A (accepting as real)
</span></span></code></pre></div><p>The validation includes some explanation on why the vulnerability is real or a false positive.
Now let&rsquo;s take a look at what the Arbiter thinks about this one:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #1: SSRF via audio_address parameter
</span></span></span><span class="line"><span class="cl">| | |
</span></span><span class="line"><span class="cl">|---|---|
</span></span><span class="line"><span class="cl">| <span class="gs">**Hunter&#39;s Claim**</span> | <span class="sb">`download_audio_from_url()`</span> accepts arbitrary URLs without validation, enabling SSRF |
</span></span><span class="line"><span class="cl">| <span class="gs">**Skeptic&#39;s Counter**</span> | (Not disputed - accepted as real) |
</span></span><span class="line"><span class="cl">| <span class="gs">**My Analysis**</span> | Code at <span class="sb">`app/utils/audio.py:71`</span> directly calls <span class="sb">`requests.get(url, ...)`</span> with NO validation of URL scheme, private IP ranges, or internal hostnames. Attacker can request <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS credentials or scan internal services. |
</span></span><span class="line"><span class="cl">| <span class="gs">**VERDICT**</span> | <span class="gs">**CONFIRMED VULNERABILITY**</span> |
</span></span><span class="line"><span class="cl">| <span class="gs">**Confidence**</span> | High |
</span></span><span class="line"><span class="cl">| <span class="gs">**Severity**</span> | CVSS 8.6 (High) - CWE-918 |
</span></span></code></pre></div><p>Basically the Arbiter compares the Hunter&rsquo;s claim and Skeptic&rsquo;s counter, judges the vulnerability including verifying the source and decides whom to award the points.</p>
<p><strong>Disproven vulnerability</strong></p>
<p>Let&rsquo;s take a look at a vulnerability that will be disproven by the Skeptic and see what the Arbiter thinks about it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### 11. MD5 Used for Task ID Generation
</span></span></span><span class="line"><span class="cl">**Location:** <span class="sb">`app/utils/common.py:28`</span>  
</span></span><span class="line"><span class="cl"><span class="gs">**CWE:**</span> CWE-328 (Use of Weak Hash)  
</span></span><span class="line"><span class="cl"><span class="gs">**Description:**</span> MD5 is used for generating task IDs. While not directly a security issue here, MD5 is cryptographically broken and its use indicates potential for similar usage in security-sensitive contexts.  
</span></span><span class="line"><span class="cl"><span class="gs">**Exploitability:**</span> LOW - Not directly exploitable  
</span></span><span class="line"><span class="cl"><span class="gs">**Severity:**</span> Medium  
</span></span><span class="line"><span class="cl"><span class="gs">**Points:**</span> +5
</span></span></code></pre></div><p>Here the vulnerability is rated as low, but still an example of <a href="https://cwe.mitre.org/data/definitions/328.html">CWE-328: Use of Weak Hash</a>.
The Skeptic attempts to disprove this one, saying that MD5 is not used for cryptographic tokens:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #11: MD5 Used for Task ID Generation (5 points)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Counter-argument:**</span>
</span></span><span class="line"><span class="cl">At <span class="sb">`app/utils/common.py:28`</span>:
</span></span><span class="line"><span class="cl"><span class="s">```python
</span></span></span><span class="line"><span class="cl"><span class="n">task_id</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">md5</span><span class="p">(</span><span class="n">combined</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">MD5 is used for generating <span class="gs">**non-security-sensitive task identifiers**</span>. Task IDs are:
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Not used for authentication
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Not cryptographic tokens
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Only used for request correlation/logging
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Using MD5 for task ID generation is perfectly acceptable - it&#39;s just creating a unique string from timestamp + UUID. The report even admits &#34;not directly exploitable&#34;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Confidence:**</span> 98%  
</span></span><span class="line"><span class="cl"><span class="gs">**Decision:**</span> DISPROVE  
</span></span><span class="line"><span class="cl"><span class="gs">**Points gained:**</span> +5
</span></span></code></pre></div><p>Here, the Arbiter agrees with the Skeptic and decides on the false positive:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #11: MD5 Used for Task ID Generation
</span></span></span><span class="line"><span class="cl">| | |
</span></span><span class="line"><span class="cl">|---|---|
</span></span><span class="line"><span class="cl">| <span class="gs">**Hunter&#39;s Claim**</span> | MD5 is cryptographically broken |
</span></span><span class="line"><span class="cl">| <span class="gs">**Skeptic&#39;s Counter**</span> | MD5 is fine for non-security identifiers |
</span></span><span class="line"><span class="cl">| <span class="gs">**My Analysis**</span> | The Skeptic is <span class="gs">**CORRECT**</span>. Task IDs at <span class="sb">`common.py:28`</span> are used for request correlation/logging, not for authentication or integrity. MD5 is perfectly acceptable for generating unique identifiers. Collision resistance is irrelevant here. |
</span></span><span class="line"><span class="cl">| <span class="gs">**VERDICT**</span> | <span class="gs">**FALSE POSITIVE**</span> |
</span></span><span class="line"><span class="cl">| <span class="gs">**Confidence**</span> | High |
</span></span></code></pre></div><h2 id="running-the-experiment">Running the experiment</h2>
<p>In addition to the multi-stage analysis involving the trio, we will also mix models a bit to check for difference in quality and cost-quality ratio. To run the experiment, I used Claude Code with Opus 4.6 and Sonnet 4.6 as the models. Having saved the prompts as files, one easily execute <code>claude</code> CLI in autonomous mode and reference the needed output files when running the Skeptic and Arbiter stages. To prepare the summary, I used <a href="https://github.com/dlupiak/claude-session-dashboard">Claude session dashboard</a> v0.4.5 for a Web UI visualizing the Claude sessions.</p>
<p>Here are the different configs for the runs:</p>
<ul>
<li>Sonnet, Opus: Baseline prompt with the given model.</li>
<li>HSA-A: Sonnet 4.6 as Hunter and Skeptic with Opus 4.6 as Arbiter</li>
<li>HSA-B: Sonnet 4.6 as Hunter with Opus 4.6 as Skeptic and Arbiter</li>
<li>HSA-C: Opus 4.6 for all three personas</li>
</ul>
<h3 id="results">Results</h3>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th style="text-align: right">Considered</th>
          <th style="text-align: right">Confirmed</th>
          <th style="text-align: right">Rate</th>
          <th>Severities</th>
          <th>API cost</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sonnet</td>
          <td style="text-align: right">20</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">—</td>
          <td>2C / 6H / 7M / 5L</td>
          <td>$0.49</td>
          <td></td>
      </tr>
      <tr>
          <td>Opus</td>
          <td style="text-align: right">15</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">—</td>
          <td>1C / 4H / 5M / 4L / 1I</td>
          <td>$2.92</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-A</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">11</td>
          <td style="text-align: right">50%</td>
          <td>2C / 1H / 5M / 3L</td>
          <td>$5.84</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-B</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">7</td>
          <td style="text-align: right">32%</td>
          <td>2C / 0H / 2M / 3L</td>
          <td>$6.91</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-C</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">5</td>
          <td style="text-align: right">23%</td>
          <td>0C / 1H / 1M / 3L</td>
          <td>$9.70</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>The mixed model pipelines (HSA-A, HSA-B) retained a higher amount of critical issue candidates. Opus is more aggressive in disproving the identified issue candidates. The Opus only run was the most conservative and kept just five candidates, 23% of all that were identified by the Hunter.
It&rsquo;s surprising that, unlike in the single prompt baselines, the Hunter prompt in the three HSA runs resulted in Sonnet and Opus identifying the same number of candidates (22), showing how the prompt encourages broad exploration.</p>
<h3 id="vulnerability-candidates-by-type">Vulnerability candidates by type</h3>
<p>Looking at the vulnerability types (types merged using gpt-5.4 across findings for simplicity), we can check for the overlap of the baseline single prompt and the multi-prompt approach. Vulnerability candidates identified and confirmed are marked with ✅, those identified and disproven with ❌, and <code>–</code> means that this vulnerability type was not identified in the run.</p>
<table>
  <thead>
      <tr>
          <th>Vulnerability types</th>
          <th style="text-align: center">Sonnet</th>
          <th style="text-align: center">Opus</th>
          <th style="text-align: center">HSA-A</th>
          <th style="text-align: center">HSA-B</th>
          <th style="text-align: center">HSA-C</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audio URL fetching / SSRF / URL validation</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Authentication enforcement / auth bypass</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Command/path handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Container/runtime hardening</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>CORS / cross-origin policy</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Credential exposure via query strings or logs</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Cryptographic / identifier hygiene</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Error and client-facing info disclosure</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>HTTPS / secure transport enforcement</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">–</td>
      </tr>
      <tr>
          <td>Logging / audit handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Resource exhaustion / rate limiting</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Security headers / browser hardening</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Temp/static file handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Token validation strength</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>WebSocket origin handling</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
      </tr>
  </tbody>
</table>
<p>The common vulnerability types across the runs relate to the file URL processing where there is no validation in the code, to resource exhaustion where the full file (max. 2GB) is being read into memory before being processed by ffmpeg. The error in client-facing information disclosure is about potential logging of information or exceptions without sanitization of data. The Opus-only pipeline (HSA-C) was filtering out vulnerabilities that would not be easy to exploit without having gained access to the server in the first place already.</p>
<p>With the output from the different runs, the hard work would begin, meaning the actual validation of the vulnerabilities, left as the exercise to the interested reader.</p>
<h2 id="summary">Summary</h2>
<p>If you&rsquo;re into vulnerability hunting, the post leaves you with more questions open than it answers. On purpose, I used a code example that does not have a ground truth solution readily available. This way, I expected lower bias through training data or vulnerability disclosures. It certainly would be interesting to use other model sets, for example the GPT-5.x or Qwen3.5 families, which I might try another time.</p>
<p>What did we learn from the experiment? Mixing models allows for broader exploration of vulnerability candidates at a lower cost, making the hunting stage a good candidate for using cheaper models. Using a more powerful model for validation of the candidates allows for quicker pruning of results that may not matter that much. To make good use of this property, system or environment-specific considerations would need to be encoded in the prompt(s), increasing the chances that the model is getting the right context, rather than resorting to averaging based on its training data. This problem-specific context could be given at least to the Arbiter.</p>
<p>I also found reading the detailed arguments from the Hunter and the Skeptic to be useful. These debate outcomes can help in understanding the analysis results and in prioritization. As such, the prompting technique of the Hunter, Skeptic, Arbiter trio can be applied to other problems that start with an exploration and that require scoring against a certain set of criteria. It&rsquo;s easy to imagine scenarios where bugs can be identified in a code base, verified by the Skeptic who even could be running code after mutating it to validate the bug, and where the Arbiter could be performing additional scoring against business criteria or value. Same can be applied to architectural design, starting with a persona exploring the key design considerations and making a draft. Then, we let the draft to be challenged by multiple persona who are trying to argue for simplicity, cost, or certain reference architectures/patterns. In this case, the Arbiter does not need to be making a final call on solution A or B, but rather scoring solutions using a scorecard or a set of criteria informed by architectural principles.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
