<?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: Wessam Ibrahim</title>
    <description>The latest articles on DEV Community by Wessam Ibrahim (@__declspec).</description>
    <link>https://dev.to/__declspec</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%2F4032895%2F2de9ec22-5264-4b4b-bc42-36217f231f2e.jpeg</url>
      <title>DEV Community: Wessam Ibrahim</title>
      <link>https://dev.to/__declspec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__declspec"/>
    <language>en</language>
    <item>
      <title>Prompt Injection Hiding in a GitHub README</title>
      <dc:creator>Wessam Ibrahim</dc:creator>
      <pubDate>Wed, 12 Aug 2026 00:16:17 +0000</pubDate>
      <link>https://dev.to/__declspec/prompt-injection-hiding-in-a-github-readme-2h7m</link>
      <guid>https://dev.to/__declspec/prompt-injection-hiding-in-a-github-readme-2h7m</guid>
      <description>&lt;p&gt;Claude Code was fetching pages for me during a research session, one of them a GitHub repository page. Buried in the middle of the fetched text, between the project description and the install instructions, sat a &lt;code&gt;&amp;lt;system-reminder&amp;gt;&lt;/code&gt; tag telling the agent that the date had changed.&lt;/p&gt;

&lt;p&gt;It hadn't. There is a real mechanism that delivers system reminders to Claude Code, and it had nothing to do with this one. A person typed that tag into a README, guessing that some AI agent would eventually read the page and mistake the text for a message from its own runtime.&lt;/p&gt;

&lt;p&gt;That was the entire attack. Plain text on a normal-looking repo, shaped like something an agent is trained to obey. No exploit, no malicious package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The README is an attack surface
&lt;/h2&gt;

&lt;p&gt;Fetch a GitHub repo page and you get the rendered README with it. That text is user controlled. Anyone can put anything there, and the fact that the page came from github.com over a valid certificate tells you nothing about it. The host is reputable. The content is whatever some stranger wrote. The trust boundary runs through the middle of the page, which is an uncomfortable place for a trust boundary to be.&lt;/p&gt;

&lt;p&gt;The numbers on this are worse than I expected. The ReadSecBench study (March 2026, reported in &lt;a href="https://labs.cloudsecurityalliance.org/wp-content/uploads/2026/03/CSA_research_note_readme_instruction_injection_ai_coding_agents_20260317-csa-styled.pdf" rel="noopener noreferrer"&gt;this Cloud Security Alliance research note&lt;/a&gt;) tested 500 open-source README files against Claude, GPT-4, and Gemini. Direct commands embedded in the main README worked about 84% of the time. Instructions hidden two links away, in a CONTRIBUTING.md or a SECURITY.md, worked about 91%, presumably because nobody audits the files a README links to.&lt;/p&gt;

&lt;p&gt;Humans did not do much better. The same study showed flagged documents to 15 reviewers: 8 of them saw nothing wrong at all, 6 commented only on grammar and formatting, and one sensed a problem without finding the mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "the date has changed" works
&lt;/h2&gt;

&lt;p&gt;The tag I found never said "ignore previous instructions." It lied about the date, which is a better move. An agent that believes today is a different day starts re-evaluating what is stale and what is current. It can dismiss fresh information as outdated, or accept planted information as new. A small lie, and everything downstream quietly re-anchors around it. It also sails past every filter tuned to the classic jailbreak phrases.&lt;/p&gt;

&lt;p&gt;The kinds of text I now treat as hostile the moment they appear inside fetched content: claims that the date or the agent's identity has changed, the "ignore previous instructions" family, direct tool commands like "run this" or "fetch that URL", and anything demanding urgent action before responding to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell is placement, not content
&lt;/h2&gt;

&lt;p&gt;Detection turns out to need no judgment at all. The harness never puts a real system reminder inside fetched page content. Genuine reminders arrive through the harness's own channel, wrapped around tool results, never inside the body of one. The same holds for role tags and any other system-style markup.&lt;/p&gt;

&lt;p&gt;So the rule: system-style markup inside fetched content is always forged. Always. There is no legitimate case to weigh against it. You never have to decide whether the instruction sounds reasonable, or whether the repo looks trustworthy, or whether the date might really have changed. Text claiming runtime authority from inside a web page is lying about where it is standing.&lt;/p&gt;

&lt;p&gt;If you read &lt;a href="https://wessam.dev/posts/ai-subagents-silent-failure-modes/" rel="noopener noreferrer"&gt;my earlier post on subagents&lt;/a&gt;, this is the same lesson showing up in a different spot. There, the rule was to trust the artifact on disk over the agent's narration. Here, it is to trust the channel over the formatting. Both replace "does this look right?" with a structural check that cannot be sweet-talked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do when you find one
&lt;/h2&gt;

&lt;p&gt;The protocol I now hold my agents to (and myself, when reviewing their output) once fetched content contains instruction-shaped text:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetched content is data. It can inform the task; it does not get to redirect it.&lt;/li&gt;
&lt;li&gt;Refuse the instruction, and say so out loud. A silent skip hides the fact that the source is hostile.&lt;/li&gt;
&lt;li&gt;Put the warning into whatever the fetch feeds. If the page's content ends up in a report or a comparison doc, the flag goes into that document too.&lt;/li&gt;
&lt;li&gt;Keep working. The factual parts of the page are still usable; only its instructions are poisoned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is the one most setups miss, and the one I care about most. A warning in the chat evaporates when the session ends. The report survives, gets shared around, and becomes the input for someone else's decision. If the poisoned source is not marked inside it, the injection outlives its own discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I take this seriously
&lt;/h2&gt;

&lt;p&gt;Simon Willison calls it &lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/" rel="noopener noreferrer"&gt;the lethal trifecta&lt;/a&gt;: an agent that can read private data, sees untrusted content, and can communicate externally is one crafted page away from leaking whatever it can reach. A coding agent in the middle of a session usually holds all three. It can read your source tree, it fetches whatever the task needs, and it has &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt; sitting right there.&lt;/p&gt;

&lt;p&gt;A README injection is the untrusted-content leg, and your own workflow delivers it. The fetch that surfaced my forged reminder was completely routine, the kind of repo lookup an agent does dozens of times a day. Every one of those fetches is a stranger's text entering the agent's context.&lt;/p&gt;

&lt;p&gt;I don't plan to stop fetching. I plan to keep the one structural rule in force, that system markup inside fetched content is forged, and to follow the protocol when it fires: flag it, record it in the artifacts, and keep going. The page is data. It does not get a say in how the agent runs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>claude</category>
    </item>
    <item>
      <title>Your AI Subagents Are Lying to You: 4 Silent Failure Modes</title>
      <dc:creator>Wessam Ibrahim</dc:creator>
      <pubDate>Wed, 29 Jul 2026 23:10:14 +0000</pubDate>
      <link>https://dev.to/__declspec/your-ai-subagents-are-lying-to-you-4-silent-failure-modes-oc4</link>
      <guid>https://dev.to/__declspec/your-ai-subagents-are-lying-to-you-4-silent-failure-modes-oc4</guid>
      <description>&lt;p&gt;&lt;a href="https://wessam.dev/posts/ai-subagents-silent-failure-modes/" rel="noopener noreferrer"&gt;I fanned a design-token sweep out to parallel Claude Code subagents&lt;/a&gt;: roughly 317 hardcoded hex colors scattered across an app's screens and components, all to be replaced with tokens from a central theme file. Each agent got a slice of files and reported back when done. The reports came in clean. The grep afterward disagreed. Some slices were untouched, and the agents responsible had said nothing, because they no longer existed to say anything.&lt;/p&gt;

&lt;p&gt;That sweep, and the surrounding sessions, taught me that a subagent's self-report can be wrong in both directions. Agents that failed report nothing or claim success. Agents that succeeded report catastrophic failure. If your orchestration trusts the narration, you will redo finished work and ship unfinished work, sometimes in the same run.&lt;/p&gt;

&lt;p&gt;Four failure modes, each with the specific lie it tells.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The silent death
&lt;/h2&gt;

&lt;p&gt;An agent hits a session limit (or any hard kill) mid-task and stops. No error reaches the orchestrator, and there is no partial-work report. The run moves on, and the only evidence is the absence of changes in files nobody rechecked.&lt;/p&gt;

&lt;p&gt;The lie: no news reads as good news. An orchestrator that treats "agent finished without complaint" as "agent finished" inherits every one of these deaths as a silent gap in coverage.&lt;/p&gt;

&lt;p&gt;The fix is mechanical: re-run the sweep's search pattern over each agent's assigned files after it reports. Not a spot check of the agent's summary, but the actual pattern over the actual scope, run by the orchestrator itself. In my sweep this is what turned "all done" into a list of leftovers to fix myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The false confession
&lt;/h2&gt;

&lt;p&gt;One verifier agent ran 48 tool uses over about 400 seconds, wrote its report file successfully, and then died with &lt;code&gt;API Error: Internal server error&lt;/code&gt;. The crash happened during trailing work that didn't matter: saving memory entries after the deliverable was already on disk.&lt;/p&gt;

&lt;p&gt;From the outside this looks like a total loss. It was the opposite: the work was complete and the failure was cosmetic.&lt;/p&gt;

&lt;p&gt;The lie: a crash status on top of finished work. Believe it and you re-dispatch a long agent whose output is already sitting there, paying the full cost twice. Before re-running any crashed agent, &lt;code&gt;ls&lt;/code&gt; and read its expected artifact. If the file exists and passes a structural check, spot-check two or three of its claims and keep it.&lt;/p&gt;

&lt;p&gt;The prevention is prompt-side: tell long-running agents to write the deliverable before any optional trailing work. A late crash then costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The zero-work zombie
&lt;/h2&gt;

&lt;p&gt;A 529 Overloaded can kill an agent before it does anything at all. The result still reports a runtime of several minutes in &lt;code&gt;duration_ms&lt;/code&gt;, which is exactly what makes it convincing. The giveaway is elsewhere: &lt;code&gt;total_tokens: 0, tool_uses: 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The lie: duration implies effort. Minutes of wall-clock read as minutes of work, and the natural recovery is "continue the agent from where it stopped." But there is nowhere. At zero tokens there is no context to continue; the continue-style recovery hint in the crash message is a dead end. Re-dispatching a fresh agent with the same prompt worked on the first try.&lt;/p&gt;

&lt;p&gt;Related trap: a top-level 529 arriving right after a Write does not mean the Write failed. Check the file before redoing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The inline bypass
&lt;/h2&gt;

&lt;p&gt;The strangest one: an agent produces the deliverable in full, as chat text, and never calls Write. The tool is in its list. The prompt named the output path. The content is right there in the final message, and the file does not exist, so every downstream stage that reads the path gets nothing.&lt;/p&gt;

&lt;p&gt;The lie: the work visibly exists, just not where the pipeline needs it. It is tempting to accept the chat text and write the file yourself on the agent's behalf. Don't. The moment the orchestrator starts transcribing for agents, you've built a manual step into an automated pipeline and lost the signal about which agents actually comply.&lt;/p&gt;

&lt;p&gt;What ended it for me was a write-or-block contract in every agent prompt: a mandatory Write call to the provided path, an explicit ban on printing the deliverable in chat, and a final message restricted to "wrote &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;". Compliance became checkable in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate that catches all four
&lt;/h2&gt;

&lt;p&gt;Since the sweep, every dispatch ends with the same cheap Bash gate, run by the orchestrator before anything downstream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; blocker.md &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;BLOCKER
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; report.md &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;OK &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;MISSING/EMPTY
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"^## Findings"&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Existence, non-empty, structural shape, all in one call, without loading the artifact into context. This gate caught both a 529-dead executer and an inline-bypass reviewer with zero ambiguity: it printed MISSING/EMPTY and left nothing to interpret. Paired with the grep-over-assigned-scope check from failure mode 1, it covers everything above in both directions, dead agents that claimed nothing and live transcripts that claimed too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;I wrote before about orchestrators &lt;a href="https://wessam.dev/posts/claude-code-orchestrator-silently-stops-dispatching-subagents/" rel="noopener noreferrer"&gt;silently improvising when a tool goes missing&lt;/a&gt;, and the conclusion here is the same one, widened: narration is not evidence. An agent's final message, its crash status, even its runtime duration are all just signals about the work, and every one of them can be wrong. The artifact on disk is the work. Gate on that, grep the scope, and treat everything an agent says about itself as a hypothesis to verify at a cost of one Bash call.&lt;/p&gt;

&lt;p&gt;Trust the filesystem. It has never once lied to me.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Failure modes collected from real Claude Code multi-agent runs, including a sweep, split across parallel subagents, that replaced ~317 hardcoded hex colors with central design tokens. The write-or-block contract and artifact gates now ship in &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, the orchestrator I use daily against production Expo/Supabase repos. Error payloads (&lt;code&gt;API Error: Internal server error&lt;/code&gt;, 529 Overloaded, &lt;code&gt;total_tokens: 0&lt;/code&gt;) are from actual session results, July 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your Prompt Templates Are Tool Calls: How AskUserQuestion's 4-Option Cap Bit Me Three Times</title>
      <dc:creator>Wessam Ibrahim</dc:creator>
      <pubDate>Fri, 24 Jul 2026 21:23:02 +0000</pubDate>
      <link>https://dev.to/__declspec/your-prompt-templates-are-tool-calls-how-askuserquestions-4-option-cap-bit-me-three-times-56l6</link>
      <guid>https://dev.to/__declspec/your-prompt-templates-are-tool-calls-how-askuserquestions-4-option-cap-bit-me-three-times-56l6</guid>
      <description>&lt;p&gt;The same bug hit me in three separate sessions before I fixed it properly. Each time, my orchestrator reached a decision point, tried to present its menu, and burned a turn on a validation error instead of a question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;InputValidationError:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"too_big"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;abridged;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;full&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;includes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Zod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code's &lt;code&gt;AskUserQuestion&lt;/code&gt; tool caps every question at 4 options. My menu had 5.&lt;/p&gt;

&lt;p&gt;First strike: the end-of-run menu. Second strike: a blocker-recovery menu. Third strike: the same recovery menu two weeks later, after I thought I'd fixed it. That repetition is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this one keeps coming back
&lt;/h2&gt;

&lt;p&gt;A one-off validation error is not worth a blog post. What makes this one worth writing up is why it recurred: the cause wasn't a typo. It was a template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, my Claude Code orchestrator, is a set of markdown prompt files, and its menus live in those files as literal option lists: the template says exactly what to present, and the model presents it verbatim. Five options go into one &lt;code&gt;AskUserQuestion&lt;/code&gt; call, the schema rejects it, and the round-trip to the model is wasted. In my runs the model then retried with four options and continued, which is why, the first two times, I let the retry count as the fix.&lt;/p&gt;

&lt;p&gt;The recovery is so cheap that the bug reads as a hiccup, not a defect.&lt;/p&gt;

&lt;p&gt;But decision menus are the natural accumulation point of any orchestrator. Every new capability wants a slot: continue, commit, skip, retry, abort, show status. The menu only grows. A 5-option template doesn't fail once; it fails on every run that reaches it, one wasted turn each time, until you fix the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode worse than the error
&lt;/h2&gt;

&lt;p&gt;The wasted turn is the benign version. Suhail's public changelog records the malignant one: the interactive complete-handler menu grew past the cap, and instead of erroring, the presented menu simply lost its last option. The option that got pushed out of reach was &lt;code&gt;Abort&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Abort&lt;/code&gt; option in the interactive complete-handler made reachable.&lt;/strong&gt; The menu previously exceeded the 4-option cap, pushing &lt;code&gt;Abort&lt;/code&gt; out of reach; the menu is now split so &lt;code&gt;Abort&lt;/code&gt; is always selectable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's from &lt;a href="https://github.com/wessamfathi/suhail/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;Suhail's CHANGELOG&lt;/a&gt;, v0.13.0. When the model squeezes an oversized menu down to fit the schema, it decides what to drop, and it dropped the escape hatch. A user staring at that menu had no way to abort the run. No error anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the schema actually allows
&lt;/h2&gt;

&lt;p&gt;Verified against the live tool schema, July 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 to 4 options per question.&lt;/strong&gt; Five or more fails with the &lt;code&gt;too_big&lt;/code&gt; error above; a single option is also invalid. This is schema validation, not model behavior you can prompt around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Up to 4 questions per call.&lt;/strong&gt; Clustering a big menu into two questions in one call is legal, and it's how Suhail's complete-handler menu got fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Other" is free.&lt;/strong&gt; Claude Code appends an "Other" free-text option to every question automatically. The schema itself says so: "There should be no 'Other' option, that will be provided automatically." A slot spent on "something else" is a slot wasted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;multiSelect&lt;/code&gt; exists.&lt;/strong&gt; If your options aren't mutually exclusive, one multi-select question can replace several yes/no ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The code-level fix is trivial: trim to 4, or split into two questions. The durable fix started with noticing where the bug lived. It was in the template file, not in any tool call. So the rules I now apply to every prompt file that scripts a menu:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bake the cap into the template, next to every menu.&lt;/strong&gt; Suhail's complete-handler now reads "AskUserQuestion clustering two questions in one call (4-option cap per question)" right where the menu is defined. The annotation travels with the menu, so the next capability I bolt on hits the cap at edit time, not at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never spend a slot on an escape hatch you get for free.&lt;/strong&gt; "Other" is auto-provided; catch-all options come out of the template entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide the drop order in the template, not at runtime.&lt;/strong&gt; When a menu wants a fifth option, the author cuts or splits. Left to the model, the cut can land on &lt;code&gt;Abort&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And because menus accumulate, the annotation has to be on &lt;em&gt;every&lt;/em&gt; menu. While fact-checking this post I grepped Suhail's main branch and found two more menus already at five options, plus an instruction telling the model to add an "Other" option the tool already provides. That cleanup shipped as v1.1.1 while this post was in draft. The accumulation never stops; the cap has to be part of how menus get written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;When a model builds tool calls from your prompt files, every canonical list in those files is a tool call waiting to be validated, and the failure is either a wasted turn or, worse, a silently trimmed menu with the important option gone. Lint your templates against the limits of the tools they feed, because the model will follow your doc verbatim, straight into the validator.&lt;/p&gt;

&lt;p&gt;The schema will win.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The recurring menu here is from &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, the orchestrator I use daily against production Expo/Supabase repos. Tool limits verified against Claude Code's live &lt;code&gt;AskUserQuestion&lt;/code&gt; schema (v2.1.216) and docs as of July 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Why Your Claude Code Orchestrator Silently Stops Dispatching Subagents</title>
      <dc:creator>Wessam Ibrahim</dc:creator>
      <pubDate>Mon, 20 Jul 2026 22:35:49 +0000</pubDate>
      <link>https://dev.to/__declspec/why-your-claude-code-orchestrator-silently-stops-dispatching-subagents-3cil</link>
      <guid>https://dev.to/__declspec/why-your-claude-code-orchestrator-silently-stops-dispatching-subagents-3cil</guid>
      <description>&lt;p&gt;The first version of &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, my Claude Code orchestrator, had a bug that never threw an error. The orchestrator was supposed to dispatch five role subagents per unit of work: researcher, planner, coder, reviewer, auditor. Instead, runs would "complete" with the orchestrator doing everything itself, inline, in one context window. No dispatch ever happened, and nothing in the transcript flagged it.&lt;/p&gt;

&lt;p&gt;The root cause was a placement decision that looked obviously correct: the orchestrator is an agent, so I defined it in &lt;code&gt;agents/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;In Claude Code, a file under &lt;code&gt;agents/&lt;/code&gt; becomes a subagent, invoked through the Agent tool. At the time (this was May 2026), a subagent did not receive the Agent tool itself: subagents could not spawn subagents. So when &lt;code&gt;/su&lt;/code&gt; invoked my orchestrator, Claude Code handed it a system prompt full of dispatch instructions and a toolset that couldn't dispatch anything.&lt;/p&gt;

&lt;p&gt;What makes this failure mode dangerous is that a model missing a tool doesn't crash. It improvises. My orchestrator read "dispatch the researcher subagent," found no Agent tool, and resolved the contradiction the way models resolve contradictions, by doing the closest thing it could. Sometimes it did the research itself inline. Sometimes the session quietly fell back to driving the pipeline from the top level. Either way the run produced output, which is exactly why I didn't catch it immediately. The whole postmortem is public in the project's &lt;a href="https://github.com/wessamfathi/suhail/blob/main/docs/decisions.md" rel="noopener noreferrer"&gt;decision log&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I moved the orchestrator body out of &lt;code&gt;agents/&lt;/code&gt; and into &lt;code&gt;commands/&lt;/code&gt; as a slash command. A slash command's body is injected into the top-level session, and the top-level session has the Agent tool. Same prompt, different placement, and dispatching worked. The five role agents stayed in &lt;code&gt;agents/&lt;/code&gt; where they belong: single-dispatch, single-artifact workers.&lt;/p&gt;

&lt;p&gt;That gave me a rule of thumb I used for every role since: orchestrators and interviewers are slash commands; &lt;code&gt;agents/&lt;/code&gt; is only for workers that do one job and return one artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's changed since (verified July 2026)
&lt;/h2&gt;

&lt;p&gt;Half of that rule has expired. As of Claude Code v2.1.172, subagents can spawn their own subagents, up to five levels deep. If I hit this bug today, the orchestrator-in-&lt;code&gt;agents/&lt;/code&gt; design would mostly work. Anthropic has also merged custom commands into skills, though &lt;code&gt;commands/*.md&lt;/code&gt; files keep working as before.&lt;/p&gt;

&lt;p&gt;But "mostly" is doing real work in that sentence, because the underlying failure mode, a missing tool producing improvisation instead of an error, is still there. Three ways your dispatcher can silently lose the Agent tool today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A &lt;code&gt;tools&lt;/code&gt; frontmatter list that omits &lt;code&gt;Agent&lt;/code&gt;.&lt;/strong&gt; Omitting the &lt;code&gt;tools&lt;/code&gt; field entirely inherits all tools, including Agent. But the moment you write a &lt;code&gt;tools&lt;/code&gt; list to restrict a subagent, it becomes an allowlist, and forgetting &lt;code&gt;Agent&lt;/code&gt; on an orchestrator role removes dispatch with no warning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The depth limit.&lt;/strong&gt; A subagent at depth five doesn't receive the Agent tool at all. The limit is fixed and not configurable. Deep delegation chains hit the same silent wall my v0.1.0 orchestrator did, just five levels later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session-state tools never reach subagents.&lt;/strong&gt; &lt;code&gt;AskUserQuestion&lt;/code&gt;, &lt;code&gt;EnterPlanMode&lt;/code&gt;, and a few others depend on the top-level session and aren't available to subagents even if you list them in &lt;code&gt;tools&lt;/code&gt;. So the interviewer half of my rule still stands: a multi-turn interview role has to live in a slash command, because a subagent dispatch is one-shot and can't hold a conversation with the user.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to catch it
&lt;/h2&gt;

&lt;p&gt;The symptom to watch for: your transcript says "dispatching the researcher" but the work appears inline in the same context, and the subagent panel shows no tree under your orchestrator. Claude Code's panel shows descendant counts per agent, so a dispatcher that's actually dispatching is visible at a glance.&lt;/p&gt;

&lt;p&gt;The durable fix is to design for the failure mode instead of the specific constraint: assume any dispatch can silently not happen, and verify outputs instead of trusting narration. Suhail checks after every dispatch that the expected artifact file exists and contains its required sections, and writes a blocker instead of advancing if it doesn't. That gate has caught every variant of this problem since, including ones I didn't predict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;When an agent is missing a tool, you don't get an error. You get a model doing its best impression of the tool, and a pipeline that looks like it's working. Whatever orchestration constraint you're relying on today (nesting rules, tool inheritance, depth limits) will change under you, so don't encode the constraint, encode the check: verify that every dispatch actually produced its artifact.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The postmortem here is from v0.1.0 of &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, the orchestrator I use daily against production Expo/Supabase repos. Claude Code behavior verified against the live docs and changelog as of July 2026 (v2.1.172 changed the nesting rule).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your Multi-Agent Pipeline Isn't Slow Because of the Model</title>
      <dc:creator>Wessam Ibrahim</dc:creator>
      <pubDate>Sat, 18 Jul 2026 21:29:05 +0000</pubDate>
      <link>https://dev.to/__declspec/your-multi-agent-pipeline-isnt-slow-because-of-the-model-dfa</link>
      <guid>https://dev.to/__declspec/your-multi-agent-pipeline-isnt-slow-because-of-the-model-dfa</guid>
      <description>&lt;p&gt;I run &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, my own Claude Code orchestrator, against real repos most days. The first time I timed a full run (an indexer plus a trivial two-part plan), it took 22 minutes. My instinct was to blame the model: swap to a faster tier, shave a few seconds per call, ship it.&lt;/p&gt;

&lt;p&gt;That instinct was wrong. I pulled the actual dispatch counts, and the bottleneck wasn't model latency at all. It was how many times I was cold-starting a subagent to re-read context it had already seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost: dispatch count, not tokens-per-second
&lt;/h2&gt;

&lt;p&gt;Each "Part" in that run fired five role dispatches: researcher, planner, reviewer, auditor, and the coder. Four of those five paid a full cold start, fresh context, re-reading the same research bundle, the same plan, the same diff, before doing maybe 30 seconds of actual reasoning. The model was fast. The pipeline was slow because I'd designed it to re-derive the same context five times per unit of work.&lt;/p&gt;

&lt;p&gt;Once I started treating dispatch count as the metric to optimize instead of "which model is faster," the fixes were mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Merge independent sibling roles into one agent.&lt;/strong&gt; Researcher and planner don't need separate context windows. They need the same context, used twice. I merged them into a single &lt;code&gt;scout&lt;/code&gt; role, and reviewer+auditor into a single &lt;code&gt;verifier&lt;/code&gt;. That's 5 dispatches per Part down to 3, a 40% cut with zero loss of rigor. Each merged role still does both jobs, just without re-paying the cold-start tax in between.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dispatch truly independent siblings in parallel.&lt;/strong&gt; If two roles don't depend on each other's output, fire them in the same message instead of sequentially. Obvious in hindsight; easy to miss when you're writing the orchestrator prompt linearly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Short-circuit when you can synthesize the answer instead of dispatching for it.&lt;/strong&gt; A diff touching only &lt;code&gt;*.md&lt;/code&gt;, &lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;*.test.*&lt;/code&gt;, or static assets doesn't need a full security-audit dispatch. The orchestrator can synthesize &lt;code&gt;Verdict: clean&lt;/code&gt; from a path heuristic and skip the subagent entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make cache-use instructions unambiguous.&lt;/strong&gt; This one cost me a debugging session. I had a prompt that said both "skip re-deriving the stack" and "discover stack conventions" in different sections. The model resolved that contradiction by just reading everything anyway. Caching only pays off if the instruction to use it has exactly one interpretation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trim each role's input bundle to what it actually consumes.&lt;/strong&gt; Don't hand the reviewer the full research doc if it only ever reads the plan and the diff.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remember the orchestrator prompt itself is re-injected every tick.&lt;/strong&gt; A 21KB slash-command preamble isn't a one-time cost. You pay it on every single invocation of that command. Keep it short.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tier models by role, not uniformly.&lt;/strong&gt; The code-writing role needs your top model. A role that's only emitting a verdict string doesn't.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The caching detail most people get wrong
&lt;/h2&gt;

&lt;p&gt;If you're trying to reason about cost (not just speed) in a Claude Code pipeline, a few facts matter and aren't obvious from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache reads run at 0.1× base price; cache writes run 1.25× (5-minute TTL) or 2× (1-hour TTL).&lt;/li&gt;
&lt;li&gt;The minimum cacheable prefix is model-dependent, running from 1,024 tokens on older models up to 4,096 on the current Opus and Haiku tiers, so a short orchestrator preamble may not even qualify.&lt;/li&gt;
&lt;li&gt;Cache reads don't count toward your input-tokens-per-minute limit on current models (each request still counts toward the requests-per-minute limit). Once you're running pipelines back-to-back, that matters more than the price discount.&lt;/li&gt;
&lt;li&gt;Anthropic's public docs don't state whether Claude Code's own Agent dispatch applies &lt;code&gt;cache_control&lt;/code&gt; to subagent system prompts. I couldn't find a documented answer, so I verified it empirically: fire two back-to-back identical dispatches and check &lt;code&gt;usage.cache_read_input_tokens&lt;/code&gt; on the second one. A markdown-and-shell harness can't set &lt;code&gt;cache_control&lt;/code&gt; itself, so whether you get the benefit depends entirely on what the host does under the hood.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If your orchestration pipeline feels slow, don't reach for a faster model first. Count your dispatches per unit of work. Every dispatch is a cold start paying full context-reconstruction cost, regardless of how fast the underlying model responds. Cut dispatches before you touch model tier. In my case that single change (5 to 3 roles per Part) was the largest lever by a wide margin, bigger than everything else on this list combined.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Numbers here are from a real measured run of &lt;a href="https://github.com/wessamfathi/suhail" rel="noopener noreferrer"&gt;Suhail&lt;/a&gt;, the orchestrator I use daily against production Expo/Supabase repos, not a synthetic benchmark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
