TLDR: When you shell out to claude -p (headless Claude) in a subprocess, MCP tools don't just appear — you have to wire them in explicitly. And the flag you'll reach for first is the wrong one.

The Setup

I've been building an autonomous meeting-prep agent — Apollo, my AI executive assistant — that polls my calendar every 5 minutes via launchd, spots upcoming meetings, researches the counterparty, and silently drops a prep note into my Obsidian vault before I get on the call.

The whole thing shells out to claude -p for each research run.

Clean. Lightweight. Unattended.

The Wall

I wanted the agent to pull Fathom (my AI meeting notes tool) transcripts as part of the research — check whether I'd spoken with this person before, what we talked about.

Fathom has an MCP server. I have it registered globally. Works great in my main Claude Code session.

But inside the subprocess? Nothing. Silent. Like the MCP didn't exist.

What I Tried First (Wrong)

--dangerously-skip-permissions.

It worked! The subprocess could suddenly see Fathom, see email, see everything.

And that's exactly the problem.

--dangerously-skip-permissions is just bypassPermissions with a longer name — it auto-approves every tool call, no questions asked. An unattended agent with send-email access and zero friction is a loaded gun. I was one prompt-injection away from the agent sending an email I didn't write to someone I didn't intend.

Hard no.

The Fix That Actually Worked

Two flags, used together:

  • --permission-mode dontAsk — deny-by-default. Any tool not on the allowlist aborts cleanly. No prompt. No hang. Just stops. Perfect for headless.
  • --allowedTools "mcp__fathom__get_meetings,..." — an explicit comma-joined list of exactly the tools this agent needs.

The gotcha: bare mcp__fathom does not wildcard the whole server. You enumerate every tool you want, by name, in the format mcp__<server>__<tool>. (Fathom has 9 tools. I listed all 9.)

And pass --allowedTools as one comma-joined argument, not space-separated variadic — the variadic form can silently let --allowedTools swallow your --disallowedTools block and void the deny rules entirely.

One more thing that surprised me: each claude -p call spawns a fresh MCP subprocess. No shared process between runs. Which means if I edit the MCP server code, the next run picks it up automatically. Hot-reload for free.

Why This Matters to Me

The meeting-prep agent writes to my vault and could theoretically fire an email on my behalf. Nobody is watching. That asymmetry — reads are fine, SEND/CREATE is the line — is why deny-by-default isn't paranoia, it's just good engineering. The allowlist is the contract between me and my agent: here's exactly what you're allowed to touch.

P.S. If you're building any kind of autonomous agent that runs unattended and has reach into communications tools — this is the pattern. Enumerate the tools. Deny the rest. Sleep at night.