◉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.

antigravity468gemini20gatewayauthentication10integrations21

✦ 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 $10 for lifetime access
View Membership →

Related Articles

⬡ Integrations2026-10-17
When Antigravity's Browser Agent Stops at the Login Screen: Four Things to Check
The browser agent came back with only a sign-in page. Here are four checks, built on the fact that it runs in a separate Chrome profile, to get its login state sorted.
⬡ Integrations2026-10-07
The Image Model Shuts Down on October 29 — Find Every Call in Your Agent Setup and Cut Over with a Way Back
The Gemini API deprecations table lists a shutdown date of October 29 for the image model gemini-3.1-flash-image. Here is how I locate every call scattered across configs and code, match them to a ledger, and fix a cutover date and a fallback in advance — with scripts that run.
⬡ Integrations2026-05-24
Catching "Running but Doing Nothing" Antigravity Subagents — A 3-Layer Observability Pattern
Logs green, output zero — the silent failure every overnight Antigravity subagent eventually produces. A three-layer design (Heartbeat / Output Trace / Decision Log) that tells missed starts, stale inputs, and write-less loops apart, with the SQL and code to copy.