<?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: Josselin Guarnelli</title>
    <description>The latest articles on DEV Community by Josselin Guarnelli (@josselin_guarnelli).</description>
    <link>https://dev.to/josselin_guarnelli</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%2F3848245%2F089559f9-b364-4cb0-ac4a-478a36fe6965.jpeg</url>
      <title>DEV Community: Josselin Guarnelli</title>
      <link>https://dev.to/josselin_guarnelli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/josselin_guarnelli"/>
    <language>en</language>
    <item>
      <title>We Scanned 16 AI Agent Repos. 76% of Tool Calls Had Zero Guards.</title>
      <dc:creator>Josselin Guarnelli</dc:creator>
      <pubDate>Sat, 28 Mar 2026 21:58:45 +0000</pubDate>
      <link>https://dev.to/josselin_guarnelli/we-scanned-16-ai-agent-repos-76-of-tool-calls-had-zero-guards-5c2h</link>
      <guid>https://dev.to/josselin_guarnelli/we-scanned-16-ai-agent-repos-76-of-tool-calls-had-zero-guards-5c2h</guid>
      <description>&lt;p&gt;We scanned 16 open-source AI agent repositories — both agent frameworks (CrewAI, PraisonAI) and production agent applications (Skyvern, Dify, Khoj, and others) that ship real business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;76% of tool calls with real-world side effects had zero protective checks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No rate limits. No input validation. No confirmation steps. No auth checks.&lt;/p&gt;

&lt;p&gt;An important nuance: you'd expect framework code to lack guards — it's template code, and adding guards is the implementor's job. But the same pattern holds in production agent applications with real business logic. Skyvern (browser automation, 595 files): 76% unguarded. Dify (LLM platform, 1000+ files): 75% unguarded. The frameworks aren't the problem — the problem is that nobody adds guards when they build on top of them either.&lt;/p&gt;

&lt;p&gt;This means a single prompt injection — or a simple hallucination — could trigger hundreds of unvalidated database writes, unchecked HTTP requests to arbitrary URLs, or file deletions without confirmation.&lt;/p&gt;

&lt;p&gt;Here's what we found, how we found it, and how you can audit your own agent code in 60 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Scanned
&lt;/h2&gt;

&lt;p&gt;We analyzed 16 open-source repos in two categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent applications&lt;/strong&gt; — repos that ship real business logic: browser automation agents, AI assistants, LLM platforms with tool-calling capabilities. These are the repos where guards &lt;em&gt;should&lt;/em&gt; exist because the code runs in production against real databases and APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent frameworks&lt;/strong&gt; — repos like CrewAI and PraisonAI that provide scaffolding for building agents. Framework code is intentionally generic — it exposes tool call patterns without business-specific guards, because that's the implementor's responsibility.&lt;/p&gt;

&lt;p&gt;We report findings for both categories, but the story that matters is the application layer: even when developers build on top of frameworks and add their own logic, the guards don't show up.&lt;/p&gt;

&lt;p&gt;For each repo, we asked a simple question: &lt;strong&gt;which functions can change the real world, and which ones have guards?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A "tool call with side effects" is any function that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write to a database (&lt;code&gt;session.commit()&lt;/code&gt;, &lt;code&gt;.save()&lt;/code&gt;, &lt;code&gt;.create()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Delete data (&lt;code&gt;session.delete()&lt;/code&gt;, &lt;code&gt;os.remove()&lt;/code&gt;, &lt;code&gt;shutil.rmtree()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Make HTTP write requests (&lt;code&gt;requests.post()&lt;/code&gt;, &lt;code&gt;httpx.put()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Process payments (&lt;code&gt;stripe.Charge.create()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Send emails or messages (&lt;code&gt;smtp.sendmail()&lt;/code&gt;, &lt;code&gt;slack_client.chat_postMessage()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Invoke another agent (&lt;code&gt;graph.ainvoke()&lt;/code&gt;, &lt;code&gt;agent.execute()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Execute dynamic code (&lt;code&gt;exec()&lt;/code&gt;, &lt;code&gt;eval()&lt;/code&gt;, &lt;code&gt;importlib.import_module()&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A "guard" is any check that protects that call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input validation (&lt;code&gt;Field(le=10000)&lt;/code&gt;, &lt;code&gt;@validator&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Rate limiting (&lt;code&gt;@rate_limit&lt;/code&gt;, &lt;code&gt;@throttle&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Auth checks (&lt;code&gt;Depends()&lt;/code&gt;, &lt;code&gt;Security()&lt;/code&gt; in FastAPI)&lt;/li&gt;
&lt;li&gt;Confirmation steps (&lt;code&gt;confirm&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt; in function body)&lt;/li&gt;
&lt;li&gt;Idempotency (&lt;code&gt;idempotency_key&lt;/code&gt;, &lt;code&gt;get_or_create&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Retry bounds (&lt;code&gt;max_retries=&lt;/code&gt;, &lt;code&gt;@retry(stop=stop_after_attempt())&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  By repo
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Tool calls&lt;/th&gt;
&lt;th&gt;Unguarded&lt;/th&gt;
&lt;th&gt;%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skyvern&lt;/td&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;595&lt;/td&gt;
&lt;td&gt;452&lt;/td&gt;
&lt;td&gt;345&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dify&lt;/td&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;1000+&lt;/td&gt;
&lt;td&gt;1,009&lt;/td&gt;
&lt;td&gt;759&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PraisonAI&lt;/td&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1,028&lt;/td&gt;
&lt;td&gt;911&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;348&lt;/td&gt;
&lt;td&gt;273&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Full results for all 16 repos: &lt;a href="https://github.com/Diplomat-ai/diplomat-agent/blob/main/REALITY_CHECK_RESULTS.md" rel="noopener noreferrer"&gt;REALITY_CHECK_RESULTS.md&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What we found unguarded
&lt;/h3&gt;

&lt;p&gt;Across all repos, the most common unguarded categories were database writes, database deletes, HTTP write requests, subprocess/exec/eval calls, LLM calls, and email/messaging. The pattern is consistent: the more dangerous the action, the less likely it has guards.&lt;/p&gt;

&lt;p&gt;A note on methodology: subprocess/exec/eval calls are a different class of risk — these should generally be eliminated entirely, not guarded. The scanner also prioritizes recall over precision: we'd rather flag a function that might be fine than miss one that isn't. Based on manual review, the false positive rate is roughly 15-20% — mostly from generic &lt;code&gt;.save()&lt;/code&gt; calls that turn out to be config or file operations rather than database writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters For AI Agents Specifically
&lt;/h2&gt;

&lt;p&gt;You might think: "Unguarded function calls exist in every codebase. What makes agents special?"&lt;/p&gt;

&lt;p&gt;The difference is &lt;strong&gt;who calls these functions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a traditional web app, a human user triggers actions through a UI with built-in constraints — forms with validation, buttons with confirmation dialogs, rate limits per session.&lt;/p&gt;

&lt;p&gt;In an agent, an &lt;strong&gt;LLM decides which functions to call, with what arguments, how many times&lt;/strong&gt;. The LLM doesn't know your business rules. It doesn't understand that calling &lt;code&gt;refund()&lt;/code&gt; 200 times in a loop is catastrophic. And if an attacker crafts a prompt injection, the LLM will happily execute whatever functions it has access to — as many times as it's told.&lt;/p&gt;

&lt;p&gt;Without guards in the code, there's nothing between the LLM's decision and the real-world consequence.&lt;/p&gt;

&lt;p&gt;A concrete example from our scan: &lt;a href="https://github.com/khoj-ai/khoj" rel="noopener noreferrer"&gt;Khoj&lt;/a&gt;, an open-source AI assistant, exposes a function called &lt;code&gt;ai_update_memories&lt;/code&gt; that lets the LLM delete and replace user memories. It calls &lt;code&gt;session.delete()&lt;/code&gt; followed by &lt;code&gt;session.add()&lt;/code&gt; with no confirmation, no rate limit, and no validation on the content. A single adversarial prompt could wipe and replace a user's entire memory store.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Built the Scanner
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://github.com/Diplomat-ai/diplomat-agent" rel="noopener noreferrer"&gt;diplomat-agent&lt;/a&gt;, an AST-based static analyzer for Python. It uses Python's built-in &lt;code&gt;ast&lt;/code&gt; module — zero required dependencies. Optional: &lt;code&gt;rich&lt;/code&gt; for colored terminal output.&lt;/p&gt;

&lt;p&gt;Why AST and not regex?&lt;/p&gt;

&lt;p&gt;Regex pattern matching misses most real-world code patterns. A function call like &lt;code&gt;db.session.commit()&lt;/code&gt; can appear as a direct call, nested inside a try/except, called through a variable alias, or buried three levels deep in a helper function. AST understands the code structure — it parses the actual syntax tree, not text patterns.&lt;/p&gt;

&lt;p&gt;The scanner walks every Python file in your project (excluding tests, migrations, venv, examples, and other non-production directories), visits every function definition, and for each function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds all calls that match side-effect patterns (DB writes, HTTP calls, deletes, payments, etc.)&lt;/li&gt;
&lt;li&gt;Finds all guards in scope (validators, rate limits, auth checks, confirmation steps)&lt;/li&gt;
&lt;li&gt;Outputs a verdict: UNGUARDED, PARTIALLY_GUARDED, GUARDED, or LOW_RISK (for read-only functions)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The default output is a terminal report showing every finding with its verdict. You can also generate a &lt;code&gt;toolcalls.yaml&lt;/code&gt; registry (with &lt;code&gt;--format registry&lt;/code&gt;) — a committable inventory of every function with side effects, the guards present or missing, and actionable hints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It On Your Own Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;diplomat-agent
diplomat-agent &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Zero config, zero required dependencies. Takes about 2-3 seconds on a 1000-file repo.&lt;/p&gt;

&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;diplomat-agent — governance scan

Scanned: ./my-agent
Tools with side effects: 12

⚠ send_report(endpoint, payload)
  Rate limit:             NONE
  → Risk: agent could exhaust external API quota with 200 calls
  ⤷ no rate limit · no auth check
  Governance: ❌ UNGUARDED

⚠ send_notification(user_id, message)
  Rate limit:             NONE
  → Risk: agent could send 200 messages — spam risk
  ⤷ no rate limit · no auth check
  Governance: ❌ UNGUARDED

✓ process_order(order_id)
  Write protection:       Input Validation (FULL)
  Rate limit:             Rate Limit (FULL)
  Governance: ✅ GUARDED

────────────────────────────────────────────
RESULT: 8 with no checks · 3 with partial checks · 1 guarded (12 total)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What To Do When You Find Gaps
&lt;/h2&gt;

&lt;p&gt;For each unguarded tool call, you have four options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; — add validation, rate limiting, or confirmation in code. The next scan picks it up automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acknowledge it&lt;/strong&gt; — if the function is intentionally unguarded or protected elsewhere, add &lt;code&gt;# checked:ok&lt;/code&gt; as a comment:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# checked:ok — protected by API gateway
&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ALERT_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add it to CI&lt;/strong&gt; — block PRs that introduce new unguarded tool calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diplomat-agent &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on-unchecked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you commit &lt;code&gt;toolcalls.yaml&lt;/code&gt; as a baseline, only &lt;em&gt;new&lt;/em&gt; findings block — no noise on legacy code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review the inventory&lt;/strong&gt; — the &lt;code&gt;toolcalls.yaml&lt;/code&gt; file is meant to be committed and reviewed in PRs. When someone adds a new function that can delete data, it shows up in the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;We're building AI agents that can modify databases, send money, delete files, and call external APIs — and we're giving them zero guardrails in code.&lt;/p&gt;

&lt;p&gt;The OWASP Top 10 for Agentic Applications (released December 2025) explicitly recommends maintaining a complete inventory of all agentic components, their permissions, and their capabilities. The EU AI Act (enforceable August 2026) requires documenting system capabilities and human oversight measures for high-risk AI systems.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toolcalls.yaml&lt;/code&gt; is a step toward that. It's not a complete governance solution — it's a starting point. You can't govern what you can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Diplomat-ai/diplomat-agent" rel="noopener noreferrer"&gt;github.com/Diplomat-ai/diplomat-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;pip install diplomat-agent &amp;amp;&amp;amp; diplomat-agent .&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scanner is open source, the findings are reproducible. Run it on your agent code and tell me what you find.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What unguarded tool calls are hiding in your agent code? Run the scan and share your results — I'll respond to every comment.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
