Published on

Gemini image generation in Claude Code with a four-layer fallback

Gemini image generation in Claude Code with a four-layer fallback
Authors
On this page

For weeks, getting a Gemini image inside Claude Code meant a four-step ritual. Open Chrome. Copy a curl command from DevTools. Paste it into the chat. Wait for the cookies to be parsed.

Then, every two hours, do it all again.

A developer at a laptop points at a chat bubble while a giant hand uses tweezers to drop a cookie into it, surrounded by browser windows and a clock.

I got tired of being the cookie courier, so I built a four-layer fallback pipeline to get rid of that manual loop. Here's how it works, and what I learned about why the obvious shortcuts fail.

Diagram: a gemini_generate_image call falls through four fallback layers until one returns an image path

Generated automatically by the system described in this post.

The problem: Google's image-gen SIDCC

Gemini's web API gates image generation behind a capability token called SIDCC. It's a short-lived cookie that Chrome's JavaScript challenge engine only issues during actual image generation.

That makes it slippery:

  • It doesn't appear in SQLite (Chrome stores a weaker version there).
  • Headless browsers can't mint it.
  • It can't be moved between browser profiles.

Every approach I tried ran into the same wall:

  • Copy Profile 7 cookies -> server rejects (device-bound sessions)
  • Playwright with injected cookies -> regular SIDCC, blocked
  • CDP / AppleScript -> either inaccessible or wrong token type
  • Playwright headless -> no SIDCC at all

The image-gen SIDCC is a deliberate security boundary. You can't simply bypass it. You have to work around it in the architecture.

The solution: a four-layer fallback pipeline

So instead of fighting the boundary, I built around it with four independent paths:

PriorityLayerMechanismAuth / Prerequisite
1Google Generative Language APIDirect official API callAuto-extracted gcloud keys
2Background Cookie DaemonReal-time Chrome cookie captureSilent daemon sync
3Browser AutomationPlaywright persistent profileOne-time interactive login
4Pollinations.aiPublic generation endpointZero auth, high availability

Each path tries, and if it fails, the call falls through to the next one. The agent sees a result either way.

Layer 1: Google Generative Language API

The machine already had gcloud credentials configured. Using the API Keys Management API (apikeys.googleapis.com), I automatically retrieved existing keys from the account's gen-lang-client-* AI Studio projects. No manual key creation needed.

async def _api_generate_image(prompt: str) -> list[str]:
    api_key = os.environ.get("GEMINI_API_KEY", "")
    # Auto-retrieved from local settings if not in env
    model = "gemini-2.5-flash-image"
    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
    # execute request...

When quota allows, this returns a native Gemini image right away.

A macOS LaunchAgent (com.nadimtuhin.gemini-cookie-daemon) watches Chrome Profile 7's Cookies SQLite every 300ms. Whenever the cookies change, it updates ~/.config/gemini-mcp/latest-cookies.json and settings in real time.

The nice part: if I generate an image in Chrome normally, the daemon captures the resulting image-gen SIDCC immediately, and the HTTP path starts working on its own for the next calls.

Layer 3: Browser automation

A Playwright persistent profile (~/.config/gemini-mcp/browser-profile) gives an isolated browser context with a one-time login. After setup, Chrome handles SIDCC rotation naturally on every generation. No manual cookie management.

Layer 4: Pollinations.ai

When local quotas are exhausted and browser profiles are locked, Pollinations.ai is the zero-auth safety net. No API key, no account, no cookies:

async def _pollinations_generate_image(prompt: str) -> list[str]:
    url = f"https://image.pollinations.ai/prompt/{encoded}?nologo=true&model=flux"
    async with cffi.AsyncSession(impersonate="chrome110") as s:
        resp = await s.get(url, timeout=60)
    # save artifact to disk...

What "fully automatic" actually looks like

The first time the pipeline ran, it needed zero interaction from me:

API gen: quota exceeded -> falling back
StreamGenerate: blocked (regular SIDCC) -> trying Pollinations.ai
[SUCCESS] Saved image to ~/Pictures/gemini/gemini_20260531_051749_0.jpg

Claude Code asked for an image. An image appeared. No curl copying, no DevTools, no pasting.

Key takeaways

  1. Google session binding is device-level: Copying cookies between Chrome profiles produces byte-identical values that the server still rejects. The device binding is enforced server-side.
  2. The image-gen SIDCC is memory-resident: Chrome stores a standard 74-character SIDCC in SQLite. The image-gen capability token only exists in process memory and only shows up after an actual image run.
  3. Layered fallbacks beat fragile perfection: The API key has quotas. The daemon depends on Chrome activity. Browser automation needs a profile. Together, they cover 100% of generation requests.
  4. Local credentials are underused assets: Existing CLI credentials (gcloud) can negotiate access to internal API keys without asking the user for secrets.

Architecture files

ComponentResponsibility
scripts/gemini_cookie_daemon.pyPolls Chrome cookies, updates cache
scripts/gemini_browser_imagegen.pyPlaywright UI automation fallback
scripts/gemini_mcp_setup.pyOne-time browser profile initialization
scripts/update_gemini_cookies.pyFallback curl parser to local configuration
com.nadimtuhin.gemini-cookie-daemon.plistAuto-start background daemon

The MCP server chains these in priority order. Claude Code calls gemini_generate_image, gets back an artifact path, and keeps going without friction.