Setting chromedriver proxy with Selenium using Python

If you need to use proxy with python and Selenium library with chromedriver you usually use the following code:

chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--proxy-server=%s' % hostname + ":" + port)
driver = webdriver.Chrome(chrome_options=chrome_options)

This setup works well for basic proxies without authentication. However, if your proxy requires a username and password (login with a username and password), additional steps are necessary. Below, we’ll explore how to set up an authenticated proxy.

Using BotProxy for Authenticated Proxies

Chrome no longer lets an automated browser load an extension, and the proxy authentication recipe that used to live on this page went with it. Here is what replaces it, from the simplest option to the most involved.

What changed. Chrome has never accepted credentials on the command line — --proxy-server=http://user:pass@host:port silently drops the user:pass part. So everyone generated a tiny extension that answered the challenge from chrome.webRequest.onAuthRequired, zipped it, and side-loaded it with --load-extension. Chrome 142 removed that switch, along with the --disable-features=DisableLoadExtensionCommandLineSwitch escape hatch that had been keeping it alive for one more release. Chrome for Testing and unbranded Chromium still accept it; Chrome-branded builds do not, and that is what most people run.

If your script suddenly opens a browser that cannot reach anything, or throws up a proxy password prompt nobody types into, this is why.

1. IP authentication: no credentials at all

The shortest path is to stop authenticating with a password. Add the machine's IP address to your proxy user's whitelist in the dashboard and point Chrome straight at the endpoint:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://x.botproxy.net:8080")
driver = webdriver.Chrome(options=options)

driver.get("https://httpbin.org/ip")
print(driver.find_element("tag name", "pre").text)

That is the whole setup. No credentials means no 407, nothing to intercept, no extension, and nothing a future Chrome release can take away.

The trade-off: BotProxy reads its per-request instructions from the proxy username, so a whitelisted request — which carries no username — cannot ask for a country, a location, a residential exit or a sticky session. It uses your proxy user's defaults. If that is all you need, stop here.

It also needs the address to hold still. Laptops, CI runners and containers with dynamic egress will fall off the whitelist.

2. A local proxy hop: credentials without the browser knowing

Put a small proxy on 127.0.0.1 that Chrome can talk to without authentication, and let it attach the credentials on the way out:

Selenium  ──▶  127.0.0.1:8888  ──▶  x.botproxy.net:8080  ──▶  target
              (no credentials)      (adds them)

Chrome is perfectly happy to use an unauthenticated local proxy, so the whole problem disappears — and because nothing in this arrangement depends on a browser feature, no Chrome release can break it.

We publish one, in three languages, so you can run whichever matches your stack: github.com/botproxy/selenium-chromedriver-proxy. Go, Node and Python, no dependencies beyond each language's standard library, all reading the same config and already pointed at x.botproxy.net. You supply the credentials and nothing else:

# Go: one static binary
cd go && go build -o proxy-hop . && ./proxy-hop --user pxu1000-0 --pass your-password

# Node 16+, no npm install
node node/proxy-hop.js --user pxu1000-0 --pass your-password

# Python 3.8+, standard library only
python3 python/proxy_hop.py --user pxu1000-0 --pass your-password

Then point the browser at the hop and forget about it:

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://127.0.0.1:8888")
driver = webdriver.Chrome(options=options)

Unlike IP whitelisting, this keeps every selector, because the hop builds the username for you:

./proxy-hop --user pxu1000-0 --pass your-password --country US --session batch-17

--country US for any exit in a country, --location us-ny for a specific one, --residential true for a residential exit, --session batch-17 to keep the same exit IP across a run. Need two exit countries at once? Run two hops on two ports.

The same hop works for anything else that speaks to a proxy — Playwright, Puppeteer, curl, an HTTP client in a language we have not shipped a hop for:

browser = playwright.chromium.launch(proxy={"server": "http://127.0.0.1:8888"})
const browser = await puppeteer.launch({ args: ['--proxy-server=http://127.0.0.1:8888'] });
curl -x http://127.0.0.1:8888 https://httpbin.org/ip

One caution: the hop listens without authentication, so keep it bound to 127.0.0.1. Anyone who can open a socket on that machine can spend your traffic.

3. Answering the challenge in-process, over CDP

If you would rather not run a second process, Chrome will hand the authentication challenge to your script through the DevTools Protocol. Enable the Fetch domain with handleAuthRequests, then answer Fetch.authRequired with Fetch.continueWithAuth — the event carries authChallenge.source: "Proxy", so proxy challenges are in scope. This is exactly what Puppeteer's page.authenticate() and Playwright's proxy={"username": ...} do internally.

In Python this needs an event listener, which driver.execute_cdp_cmd cannot give you — it sends commands but never receives events. Recent Selenium wraps it in BiDi:

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://x.botproxy.net:8080")
options.enable_bidi = True

driver = webdriver.Chrome(options=options)
handler_id = driver.network.add_auth_handler("pxu1000-0+US", "your-password")

Two caveats worth knowing before you build on it. Selenium's own documentation frames these handlers as Basic and Digest auth and does not commit to proxy challenges, and the Python network API is still marked internal and may change shape. Pin your Selenium version and test the path you depend on. In Java the equivalent has been stable for much longer:

((HasAuthentication) driver).register(UsernameAndPassword.of("pxu1000-0+US", "your-password"));

4. Chrome for Testing, if you need today to work

--load-extension still works on Chrome for Testing and on unbranded Chromium, so pinning your automation to those keeps the old extension recipe alive. It is a reasonable way to buy a week; it is not a fix. The switch was removed deliberately, and building a scraper on a flag that one Chromium release already deleted is borrowing time at a bad rate.

What no longer works

  • Credentials in the proxy URL. --proxy-server=http://user:pass@host:port — Chrome drops the credentials. It has never worked, and it fails silently, which is worse.
  • selenium-wire. Unmaintained, and it no longer imports on current blinker releases.
  • The generated MV3 extension, which is what this page used to teach. It works only where --load-extension still exists.

No browser to run at all

If the reason you are running Chrome is that a page needs JavaScript rendering, rather than that you specifically want Selenium, our Browser API gives you cloud Chrome workers over plain HTTP: navigate, fill forms, screenshot, PDF, all already routed through the proxy network. No browser infrastructure, no ChromeDriver version dance, and no proxy authentication problem, because there is no local browser to authenticate.

Which one should you use

Static IP and no need for per-request geography — IP whitelisting. Anything else — the proxy hop: it keeps the selectors, works the same on every stack, and does not care what Chrome does next. Reach for CDP only if a second process is genuinely unacceptable in your environment.