ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-09-05Advanced

Pointing GOOGLE_GEMINI_BASE_URL at Your Own Gateway Does Not Change Who Holds the Key

I pointed the Antigravity CLI at a gateway of my own and recorded exactly which credentials travelled with it. Authorization is dropped across a cross-origin redirect, but the API key header survives every hop. Here is the measurement, and how I moved the key off my laptop.

antigravity453gemini18gatewayauthentication10integrations21

Premium Article

I decided to put one layer between my machine and the model API. The motivation was modest: I wanted to count, in one place, how much each project was calling.

So I wrote my own URL into GOOGLE_GEMINI_BASE_URL and stood up a tiny server that printed whatever it received. It took a few minutes to work.

Then I read the headers that had arrived, and I stopped. The API key that lives on my laptop was sitting right there, one line among the rest.

In hindsight it could not have gone any other way. I changed the destination, and I never wrote an instruction to strip the credentials. Still, the fact that one line in a config file quietly changes where a key gets delivered felt lighter than I expected it to.

Antigravity CLI 1.1.13 added direct GEMINI_API_KEY authentication: set modelProvider: "gemini" in settings.json, export the variable, and the CLI runs without signing in. The same release note mentions that you can point GOOGLE_GEMINI_BASE_URL at a custom endpoint. It is the shortest path there is, so anyone aiming at a local engine or an internal gateway ends up here.

Which is exactly why it is worth looking, once, with your own eyes at what arrives on the other side. What follows was measured on my own machine, in the setup I use as an indie developer.

Stand up a receiver and record the headers as they arrive

Start with a recording, not a guess. This server does not forward anything upstream; it just collects headers.

# echo_srv.py - a receiver that records headers and nothing else
import threading, http.server, socketserver
 
LOGS = {}
 
def make_handler(name, redirect_to=None):
    LOGS[name] = []
 
    class H(http.server.BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
 
        def do_POST(self):
            n = int(self.headers.get("content-length", 0))
            self.rfile.read(n)                      # body discarded; headers are the subject
            LOGS[name].append({"path": self.path,
                               **{k.lower(): v for k, v in self.headers.items()}})
 
            # anything other than /final gets bounced once
            if redirect_to and not self.path.endswith("/final"):
                self.send_response(307)             # 307 preserves method and body
                self.send_header("Location", redirect_to)
                self.send_header("Content-Length", "0")
                self.end_headers()
                return
 
            body = b'{"ok":true}'
            self.send_response(200)
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
 
        def log_message(self, *a):                  # keep the default access log quiet
            pass
 
    return H
 
def serve(port, handler):
    socketserver.TCPServer.allow_reuse_address = True
    srv = socketserver.TCPServer(("127.0.0.1", port), handler)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    return srv

I picked 307 rather than 302 because some implementations rewrite a POST into a GET on a 302, which adds a variable I am not trying to measure. Only the thing under test should move.

With the destination swapped and nothing else, the credentials arrive intact

The plain path first, with no redirect in the way. Whatever the client sends shows up on the receiver.

import requests
 
HDRS = {
    "Authorization":        "Bearer ADC_TOKEN",   # dummy values; no real secrets here
    "x-goog-api-key":       "KEY",
    "x-goog-user-project":  "my-project",
    "Content-Type":         "application/json",
}
 
serve(8914, make_handler("plain"))
requests.post("http://127.0.0.1:8914/a", headers=HDRS, json={}, timeout=5)
 
d = LOGS["plain"][-1]
print(sorted(k for k in d if k in ("authorization", "x-goog-api-key")))
# output: ['authorization', 'x-goog-api-key']

Both arrive. A base_url setting chooses a route; it never touches how credentials are handled. Obvious once stated, but seeing it makes the next difference much easier to read.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You can record exactly which credentials leave your machine before you point a client at a gateway or a local engine, instead of finding out later
You can design your forwarded-header list around a real asymmetry: Authorization is stripped across origins while the API key header is not
You can move the upstream key off your laptop and onto a gateway in an order that never leaves you guessing whether the block actually works
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Integrations2026-07-06
Semantic Search Over Your Own Codebase with Gemini Embeddings and sqlite-vec: An Incremental Index Keyed on Git Blob Hashes
A build log for semantic search scoped to your own repository, using Gemini embeddings and sqlite-vec. Covers an incremental pipeline that skips re-embedding unchanged files via git blob hashes, with measured index size and query latency.
Integrations2026-06-28
Where to Start Reading an Unattended Agent's Changes — A Digest for Re-Entry
How do you review the pile of changes an unattended agent left overnight? Not the full diff, not the chat log — a re-entry digest built from rule-based risk classification, per-agent review markers, and a 30-second verification pass.
Integrations2026-05-30
Handing Crashlytics Stack Traces to Antigravity — Three Weeks Across Four Apps
Paste a Crashlytics stack trace into Antigravity, let it narrow the cause, and drive the fix to the finish. After three weeks across four wallpaper apps, here is what I learned to delegate and what I kept for myself.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →