{"slug": "triaging-gmail-with-claude-subagents", "title": "Triaging Gmail with Claude Subagents", "summary": "Developer Joe Pohnelt created an email triage system using Claude Code subagents to automate Gmail management. The system uses six specialized AI agents for security analysis, relationship mapping, content summarization, labeling, and filter curation, with security analysis consuming the most tokens. The project is open-source on GitHub and demonstrates a thin orchestrator pattern with isolated subagents.", "body_md": "# Triaging Gmail with Claude Subagents\n\nThe email-triage-orchestrator running in Claude Code: ten isolated email-security-analyzer agents finished with all messages clean, then email-relationship-analyzer and email-content-summarizer running in parallel over the batch\n\n**Jump right to the code: https://github.com/jpoehnelt/subagent-email-triage.**\n\nI put together this demonstration project to triage my Gmail with a small constellation of [Claude Code](https://docs.claude.com/en/docs/claude-code) subagents that drive the [ gws](https://github.com/googleworkspace/cli) CLI. They read incoming mail, check it for security issues, analyze relations, summarize the content and extract context, apply labels, and turn recurring patterns into persistent Gmail filters.\n\n## Six agents\n\nThe **orchestrator** discovers inbox messages and hands everything else to the subagents. It looks like this:\n\n**Discover.** The orchestrator lists new, untriaged inbox mail.**Security.** One`email-security-analyzer`\n\nper message, isolated and never batched.**Relationship and summary.**`email-relationship-analyzer`\n\nand`email-content-summarizer`\n\n, run once over the security-clean batch.**Label.**`email-labeler`\n\ncreates the Gmail labels and applies them to the message.**Curate filters.**`email-filter-curator`\n\npromotes stable patterns to persistent filters.\n\n| Subagent | Job | Model |\n|---|---|---|\n`email-security-analyzer` | Deception / phishing / spoofing verdict | Sonnet |\n`email-relationship-analyzer` | Who the sender is to me | Haiku |\n`email-content-summarizer` | What the mail actually says | Haiku |\n`email-labeler` | Apply Gmail labels | Sonnet |\n`email-filter-curator` | Promote stable patterns to filters | Sonnet |\n\nI started with a fat orchestrator and thin subagents. It passed the email bodies and headers down to the subagents. I switched to a thin orchestrator that only discovers and delegates, mostly passing message IDs.\n\nThat change isn’t free, each subagent re-fetches what it needs, so the same email gets pulled from the API more than once. In return, the orchestrator never loads message bodies and headers just to pass them down. The reads are cheap and idempotent, worth repeating to keep the coordinating context lean. Also much easier to batch tasks to subagents this way.\n\n## I added memory, then removed it\n\nFor a while, some subagents had memory, notes they carried across runs, so a relationship agent wouldn’t re-learn who my coworkers are every time. I kept the memory local to the agent and saw that it was capturing a structure likely to go stale. The relationship analyzer was creating a memory file for every sender. I removed it, but would consider adding it back with a better prompt for its usage.\n\n## Most token usage goes to security\n\nHere’s the rough token split across the five subagents:\n\n| Subagent | % of token usage |\n|---|---|\n`email-security-analyzer` | 17% |\n`email-labeler` | 3% |\n`email-relationship-analyzer` | 3% |\n`email-filter-curator` | 2% |\n`email-content-summarizer` | 2% |\n\nSecurity dominates, more than the other four combined. It’s the only stage that runs once per email rather than batched. It also does the most with the email, reading all headers, inspecting the full body, etc.\n\nI started with a zero tool security subagent in the fat orchestrator pattern. This allowed hill climbing with autoresearch and some publicly available datasets of emails. Harder to implement the same with tools, but not impossible.\n\n## Tools, permissions, allowlists\n\nThe harness has a constraint that shaped everything, an agent’s tools are fixed at definition time. I can’t grant, narrow, or swap a subagent’s capabilities at runtime; its surface is the same for a newsletter as for a phishing email. Under a static tool model, least privilege means splitting the work across narrowly scoped agents, giving each exactly what its job needs.\n\nThis is enforced two ways. First, in `settings.json`\n\n. Second, a single global `PreToolUse(Bash)`\n\nhook enforces a different allowlist per agent. Per-agent hooks in frontmatter stack, so a subagent’s call gets checked against the orchestrator’s hook too, forcing the orchestrator’s allowlist to be a superset of all of them. Instead, one guard reads the calling `agent_type`\n\nfrom the payload and enforces only that agent’s `.allowlist`\n\n, so each agent is governed independently. The security analyzer’s Bash tool allowlist is three lines:\n\n```\ngws gmail users messages get\ngws gmail +read\ngws schema\n```\n\nThe labeler’s allowlist lets it modify labels but never reads a body. The guard also blocks command substitution, subshells, redirects, and pipes into anything that isn’t a read-only filter like `jq`\n\n, so an allowed `gws`\n\ncall can’t be piped into something that writes a file or runs code.\n\n## Filters and knowledge\n\nEvery label is a signal about the pattern of mail, and the **filter curator** analyzes that. It looks for reusable patterns in the labels and promotes them to Gmail filters. For example, if it notices that I consistently label messages from a certain sender as “Work”, it will create a filter that automatically applies that label to future messages from that sender. It’s forward-looking only, never backfills, and held to a strict threshold before it creates a filter rather than proposing one.\n\nThis is the one place I’d reconsider statelessness. Reading existing labels is fine as those facts live in Gmail. But the valuable version isn’t pattern-matching labels, it’s modeling my behavior over time. For example, my replies to emails, what I archive, snooze, or just ignore.\n\nThe relationship question grows in breadth, not depth. The same agent could ground its answer in Slack, a company directory, Google Contacts, or Google Calendar.\n\n## Things that were annoying\n\n**The pipeline order isn’t enforced.** “Discover → security → relationship + summary → label → curate” is a prompt instruction, not a guarantee. The hooks and allowlists bound what each agent can do, not the order the orchestrator calls them in. The critical invariant, security first, per email, never batched, rests on the model following instructions.\n\n**Subagent permissions:** Why can’t I define this more easily?\n\n**Subagents cannot call other agents.** This forces a more linear sequence and many of the challenges above.\n\nThis was a useful exploration, but I think I would want to skip doing this within the Claude Code harness next time.\n\nOpinions are my own and not the views of my employer.\n\n© 2026 by Justin Poehnelt is licensed under CC BY-SA 4.0", "url": "https://wpnews.pro/news/triaging-gmail-with-claude-subagents", "canonical_source": "https://justin.poehnelt.com/posts/triage-gmail-with-subagents/", "published_at": "2026-06-08 00:00:00+00:00", "updated_at": "2026-06-24 12:17:17.097839+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "ai-products", "developer-tools"], "entities": ["Claude Code", "Gmail", "Google Workspace", "Joe Pohnelt", "Sonnet", "Haiku", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/triaging-gmail-with-claude-subagents", "markdown": "https://wpnews.pro/news/triaging-gmail-with-claude-subagents.md", "text": "https://wpnews.pro/news/triaging-gmail-with-claude-subagents.txt", "jsonld": "https://wpnews.pro/news/triaging-gmail-with-claude-subagents.jsonld"}}