AI & Automation

The Claude Bridge: four flags, one timer, and a $200 ceiling

Edited by Jay AhnMay 17, 202610 min read1,832 words
The Claude Bridge: four flags, one timer, and a $200 ceiling

Last week I wrote about why my AI content automation system got rejected by AdSense, and ended with a promise: the next post would be about the Bridge specifically — the local Node server that lets my n8n workflows talk to Claude. That's this one. Actual code, actual stack trace, the four flags I now run with by default, and one timer I had to change yesterday afternoon.

The reason this matters past "look at my code" is that Anthropic is changing how claude --print is billed on June 15, and if you're running anything resembling what I'm running, your usage is about to become visible in a way it wasn't before. The Bridge is the leverage point for keeping that visible usage sane.

What the Bridge is, in one paragraph

Three blog sites and one social-poster workflow all need to call Claude as a writing model from inside n8n. The official path is the Anthropic API with a billing key. I'm on a Claude Max 20x subscription, so the work I'd be paying for through the API is already paid for through the subscription — provided I call Claude the way a Max subscriber is supposed to call it. The Bridge does that: it's a 1,400-line Node HTTP server listening on 127.0.0.1:5690, and when n8n posts a prompt to /run-claude, the Bridge spawns claude.exe --print as a subprocess, pipes the prompt into stdin, reads the JSON answer from stdout, and returns it. n8n doesn't see Claude. The Bridge sees Claude.

That's it. That's the entire idea. The rest of this post is about what happens when the subprocess doesn't behave.

The four flags

The spawn line in claude-bridge.js looks like this:

const proc = spawn(CLAUDE_PATH, [
    '--print', '--model', resolvedModel,
    '--strict-mcp-config',
    '--mcp-config', '{"mcpServers":{}}',
    '--no-session-persistence',
], {
    windowsHide: true,
    stdio: ['pipe', 'pipe', 'pipe'],
    env: { ...process.env, NO_COLOR: '1' }
});

Four flags. Each one is there because of a specific failure mode I watched happen on my own machine.

--print. Without it, claude.exe opens an interactive TUI and sits waiting for keystrokes. That's fine in a terminal. From a subprocess, it means the parent never gets stdout — the TUI is drawing to the console, not piping JSON. --print makes Claude behave like a Unix tool: read prompt, emit answer, exit. This one is obvious in hindsight; it wasn't obvious the first time I spent an evening wondering why my Bridge log was empty.

--strict-mcp-config. This is the one that cost me a week. The default behaviour of claude.exe is to auto-discover the MCP (Model Context Protocol) servers configured in your ~/.claude.json. I have four of them — Apify for research, Firecrawl for scraping, Gitnexus for code-graph queries, and Ruflo for memory. When Claude runs interactively, those MCPs initialize in the background and add tools that the model can call. When Claude runs as a subprocess of the Bridge, those same MCP initializers fight the Bridge for the same stdio pipes the Bridge is trying to read from. The fight isn't dramatic. Nothing crashes. The process just stops doing work, hung in a deadlock between two things both waiting to read from stdin that's already drained. --strict-mcp-config says: ignore the global config. The MCP set is exactly what I pass on the command line, nothing else.

--mcp-config '{"mcpServers":{}}'. And what I pass is: nothing. The empty object. I don't need Apify or Firecrawl from inside the Bridge; n8n calls those directly. The Bridge's only job is to ask Claude to write a blog post. So zero MCP servers is the right answer, and --strict-mcp-config plus an empty config is how you say it. Without the second flag, Claude still tries to auto-load a default set, and the deadlock returns.

--no-session-persistence. Each /run-claude call should be a clean transaction: prompt in, answer out, no memory of what came before. Without this flag, Claude writes a session file under your user directory after every call, and the next call inherits it. For a content pipeline that fires three sites a week, you do not want post #4 to read like a continuation of post #3 — they're different sites, different niches, different personas. --no-session-persistence ensures every call starts cold.

The four flags together are the difference between "the Bridge works" and "the Bridge logs are empty and I have no idea why." None of them are documented in a single place. I learned all four from chasing a single hang.

The stack trace I promised

This is the one from yesterday afternoon, May 16. I was smoke-testing the new persona-voice prompts I'd just deployed — the rewrite I described last week. The first execution came back like this in the n8n run inspector:

parseError: JSON parse failed or content too short
rawOutput:  {"err":{"message":"408 - {\"error\":\"timeout\",\"stdout\":\"\",\"stderr\":\"Timeout after 120s\"}","name":"AxiosError",
              "stack":"AxiosError: Request failed with status code 408\n    at settle (C:\\Users\\piz91\\AppData\\Roaming\\npm\\...\\axios\\lib\\core\\settle.js:20:17)\n    ..."}
qualityReport:
  wordCount: 0
  wordsOk: false
  hasTitle: false
  hasSlug: false
  passed: false

The interesting line is Timeout after 120s. That string lives in my Bridge code, not Anthropic's. It means the Bridge killed Claude before Claude was done. n8n then read the 408 response, tried to parse zero bytes as JSON, and routed to the error branch.

This is what the Bridge looked like before yesterday:

const timer = setTimeout(() => {
    if (!finished) {
        proc.kill('SIGKILL');
        finish();
        res.writeHead(408, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'timeout', stdout: '', stderr: 'Timeout after 120s' }));
    }
}, 120000);

120 seconds was fine when the prompt was 600 tokens. The new persona prompts are 1,400 tokens — Jay/Ravi/Daniel each get a voice-rules block, a forbidden-phrases list, and a bio paragraph injected upstream of the actual writing instructions. Sonnet's first-token latency stayed the same; what changed was how long the model spent thinking through a longer instruction set before it started writing. The output target is 1,400–2,200 words, and somewhere between "minor prompt change" and "200% prompt size increase" the average wall-clock time crossed the 120-second line.

The fix is two characters in one number. The thinking it surfaced is what's worth noting.

}, 220000);

Two-twenty. Not five hundred. Not "however long it takes." Two-twenty, because n8n's httpRequest node has its own timeout — 240 seconds — and the Bridge needs to lose first. If the Bridge times out at 220s, n8n gets a clean 408 with the actual error in the body, routes to the parse-error branch, and logs cleanly. If n8n times out at 240s first, the Bridge keeps spending compute on a request nobody is reading, and the next call has to queue behind a zombie. Whose timer fires first matters. This sounds obvious. It is obvious. It is also the kind of thing you only notice after you've watched both timers fire on the same request in the wrong order.

The new $200 ceiling

The reason this all matters more than it would have a month ago: on June 15, Anthropic is changing how claude --print calls count against Max subscriptions.

Before June 15: claude --print is just Claude. It draws from the same usage allowance as anything else you do with the Max plan. There is no separate meter.

After June 15: claude --print (and any call from the Agent SDK) draws from a separate monthly Agent SDK credit. For Max 5x, that's $100/month. For Max 20x — which is what I'm on — that's $200/month. When the credit runs out, the call either falls through to API rates (if you've enabled extra usage) or is refused (if you haven't).

I want to be specific about what this does to my system, because the math is the point.

A single blog post generation costs me, on the new meter, somewhere between $0.05 and $0.20 — call it $0.15 to be safe. My Phase 8a workflow now fires once a week, generates one draft per site, three sites: 3 calls × $0.15 × 4 weeks ≈ $1.80/month.

My Phase 9 workflow generates social-media copy: 3 sites × 3 channels × daily ≈ 270 calls × $0.10 (shorter outputs) = $27/month.

Add 20% headroom for retries and persona-prompt overhead: about $35/month.

I have $200/month of credit. I will use 17% of it. The ceiling matters, but it is not close.

The reason I'm writing this number down anyway is that it's the first time my automation has had a visible per-call cost attached to it, and the discipline of running the math before June 15 is the same discipline that should have been running the math before AdSense looked at the sites in the first place. What does each invocation actually cost me, and is the answer something I'd defend out loud? When the answer is "$1.80/month for a blog and $27/month for social, against a $200 ceiling I already paid for as a flat subscription," I can defend that. When the answer was previously "I don't know, I haven't counted, it's covered by my plan" — I couldn't, and that's the same shape of answer that produced the AdSense rejection.

What I'm changing

Two things, in the next week.

First, a monthly call counter inside the Bridge itself. Eight lines of code: increment on every /run-claude call, reset on the first of the month, surface the current count on the /health endpoint. The point isn't billing — Anthropic will do that for me. The point is that I want to see the number, weekly, with my own eyes, on the same dashboard I check uptime on. Things you can see, you can manage; things you only learn about at the end of a billing cycle are how you end up writing a postmortem about why your account got flagged.

Second, the daily limit gate on Phase 8a stays at 1 per site per day, and the cron stays weekly Tuesday 09:00 UTC. Six months ago I would have rationalised pushing it higher. The rationalisation would have been: "I'm only generating drafts, I'm reviewing each one, more drafts means more options." That rationalisation is exactly the path back to eighteen posts in a day. The gate is at 1 because 1 is what I can actually edit thoughtfully every week, and the system should be configured around what I can do well, not what the cron can do quickly.

What's next

The Bridge survives this week. The persona prompts are deployed. The first Tuesday cron fires May 19, three days from now, and produces three drafts — one Jay, one Ravi, one Daniel. I read all three before any of them ship. We'll see whether the voice holds up under generation pressure, or whether it collapses back into the same generic SEO mush that got us here. I'll write up that result next week.

The post after that is going to be about Ravi's side of things — the FRED API integration on distillfin, what DGS10 actually looks like as a chart, and why "data first, then take" is harder than it sounds.

— Jay

ℹ How this was written: AI-assisted and edited by Jay Ahn. See our AI Disclosure and Editorial Policy for details. This article is for informational and educational purposes only and does not constitute professional advice. AI tools, automation platforms, and technology evolve rapidly — verify information independently before making decisions based on this content.
clauden8nautomationbuild-in-publicbridgeindiemcp
SharePost on X