Proxy Toolbox
Proxy Toolbox / Guides / switchyomega-chrome

SwitchyOmega proxy setup: profiles, switch rules and the parts that break quietly

SwitchyOmega sits between Chrome and the browser proxy configuration, and it decides one thing that matters: which requests leave through an endpoint and which leave on the machine address. I have kept it running on three work machines, 40 to 60 rules on each, for long enough to collect the failures worth writing down. Almost all of them came from two places. Rule order, and the traffic that never touches the extension at all.

This guide walks the profile kinds, adding a server and its login, writing conditions by host and by pattern, the PAC route and when a script earns its place, finding the condition that wins a conflict, and moving a working configuration onto a second machine. Every snippet here is one I keep in my own backup file.

The profile kinds and what each one covers

A profile is a stored answer to a single question: for this request, where does it go? Two of them ship with the extension and cannot be edited. [Direct] sends everything out on the machine address. [System Proxy] hands the decision back to the operating system, which matters on machines where a desktop client writes its own values into the OS settings.

The three you build yourself carry the actual work. A Fixed Servers profile holds one endpoint, or one per scheme, with a bypass list attached to it. A PAC Profile runs a script and returns a decision per URL. A Switch Profile holds an ordered table of conditions, and every condition points at one of the other profiles. There is also a Virtual Profile, which behaves as a name that forwards to a real profile.

Profile kindWhat it doesWhere I use itWhat it will not do
[Direct]Every request goes out on the machine addressBaseline for comparison during a testCannot hold a bypass list of its own
[System Proxy]Defers to the operating system settingsMachines where a desktop client sets the OS valuesGive per-site control from inside Chrome
Fixed ServersOne endpoint, or one per scheme, plus a bypass listThe workhorse for a single poolDecide per URL beyond the bypass list
PAC ProfileRuns FindProxyForURL and returns a decisionConfiguration shared across a teamCarry a username and password
Switch ProfileOrdered conditions, first match winsThe top-level router on every machineSend traffic anywhere by itself
Virtual ProfileA name that forwards to a real profile20 rules that must swap endpoint in one fieldHold server details of its own

The Virtual Profile deserves a word, because it took me two rebuilds to start using one. My first Switch Profile had 31 conditions spread across four Fixed Servers profiles. Rotating the pool meant editing four profiles, and I missed one every time. Now the conditions point at a Virtual Profile named work-exit, and work-exit points at whichever Fixed Servers profile is current. One field changes, 31 rules follow.

Adding a server and telling the extension where the login goes

Open Options, pick a Fixed Servers profile, and you get a small table: protocol, server, port. The protocol dropdown carries HTTP, HTTPS, SOCKS4 and SOCKS5. By default the top row applies to every scheme; uncheck the box marked for all protocols and you get separate rows for HTTP, HTTPS and FTP, each with its own endpoint. I have used the split form exactly once, on a machine where plain HTTP requests went one way and TLS went another during a migration.

Credentials live behind the small lock icon at the end of the server row. Click it, type the username and password, save, and the extension answers the 407 challenge for you. Two details about that field are worth knowing before you rely on it. The pair is stored in extension storage in readable form, so a shared machine shares the login. And the extension needs the proxy authentication permission granted, or the browser puts up its own dialog on every new tab.

The second detail bites harder. Chrome carries no support for username and password authentication on SOCKS endpoints, at any layer, from any extension. Type a login into a SOCKS5 row and the connection still fails with a tunnel error. So for SOCKS work on a browser I bind the machine address in the provider dashboard and use the plain IP:PORT form. The pool I run against issues both list formats, IP:PORT and IP:PORT:LOGIN:PASS, which is why I keep the HTTP entries for credential logins and hand the bound form to the extension, while a profile browser such as Dolphin Anty with its own proxy field carries a SOCKS login inside the browser itself.

Two bound addresses come with the package I hold, and the binding is editable, so a laptop that moves between the office and home gets both slots. When I need the browser to speak plain HTTP with a login, I point the row at the HTTP proxy endpoints I run on and let the lock icon carry the pair.

The bypass list sits under the server table and belongs to that profile alone. It accepts hostnames, wildcards, CIDR blocks and the special token for local names:

localhost
127.0.0.1/8
::1
<local>
192.168.0.0/16
*.internal.example
grafana.example.net

Anything matching a line here goes out direct while that profile is active. The <local> token covers single-label hostnames with no dot in them, which is how an internal wiki reachable as wiki stays reachable. I learned to put the CIDR blocks in on day one: without 192.168.0.0/16 the router admin page tries to resolve through the endpoint and times out after 30 seconds.

Auto Switch: conditions by domain and conditions by pattern

A Switch Profile is a table read from the top down. Each row holds a condition type, a pattern, and a target profile. The first row whose pattern matches decides the request, and reading stops there. The bottom row is the default, and it catches everything the table missed.

Host conditions look only at the hostname. URL conditions see the whole address including scheme, path and query. That distinction explains most of the rules that quietly do nothing: a pattern written as */api/* under a Host condition can never match, because a hostname contains no slash.

Condition typePattern I would writeWhat it matchesWhere it earns its place
Host wildcard*.example.comThe bare domain and every subdomainThe default choice, covers most rows
Host wildcardshop?.example.comshop1, shop2, one character onlyNumbered front ends in a farm
Host regex`^(a\b)\d+\.example\.net$`Hostnames the wildcard form cannot expressRare, and I keep it to two or three rows
Host levelsmin 3, max 4Hostnames with 3 or 4 dot-separated labelsCatching deep subdomains in bulk
URL wildcard*://api.example.com/v2/*Scheme, host and path togetherSplitting an API off the main site
URL regex`^https://.*\.example\.com/(cart\pay)`Path branches inside one hostCheckout flows that need a separate exit
Keywordpartner-trackAny URL containing that stringTracking endpoints scattered across hosts
IP literal10.0.0.0/8Requests to a literal address in that blockInternal services reached by number
Timezone / Weekday09:00 to 19:00Requests inside a windowNight runs that must go direct

The wildcard form has one behaviour that surprises people, and the documentation spells it out: *.example.com matches example.com as well. You do not need a second row for the bare domain. Writing one anyway costs nothing except a longer table, and a longer table costs you time later when a conflict needs tracing.

Matching runs at the level of the individual request. Open one page and the table gets consulted 40 or 80 times, once per resource. A page on news.example.com pulling fonts from static.cdn-example.net and analytics from a third host produces three separate decisions, and they can land on three different profiles. This is the single most useful thing to hold in your head while debugging: the address bar tells you almost nothing about which rules fired.

Here is the shape of a table I run on the machine I use for data collection, written out in the rule list syntax the extension imports:

[SwitchyOmega Conditions]
@with result

*.internal.example +[Direct]
10.0.0.0/8 +[Direct]
*://api.target-site.com/v3/* +work-exit
*.target-site.com +work-exit
*.metrics-vendor.net +[Direct]
* +[Direct]

Nine rows in the interface, six lines here. The order is the whole design: the API row sits above the host row for the same site, so a request to api.target-site.com/v3/items takes the first of the two and the site's own pages take the second.

The rule list and the PAC file, and when a script earns its place

A Switch Profile can pull an external rule list over HTTP and refresh it on an interval. You give it a URL, pick the format, and set two targets: one profile for URLs the list matches and one for the remainder. The extension supports the AutoProxy format and its own condition syntax, and it stores the fetched copy locally so a failed refresh keeps the last good table.

I use a rule list for the set of domains shared between machines, and local conditions for anything specific to one machine. In the matching order, my own conditions are consulted first, then the rule list, then the default row. That ordering is worth confirming on your own build with two deliberately overlapping entries, because a stale assumption here produces the exact class of bug that takes an afternoon.

A PAC Profile replaces the whole table with a script. The script exports one function, receives the URL and the host, and returns a string the browser understands:

function FindProxyForURL(url, host) {
  if (isPlainHostName(host) || shExpMatch(host, "*.internal.example"))
    return "DIRECT";

  if (isInNet(host, "10.0.0.0", "255.0.0.0"))
    return "DIRECT";

  if (shExpMatch(host, "*.target-site.com"))
    return "PROXY 203.0.113.41:8080; PROXY 203.0.113.42:8080; DIRECT";

  if (shExpMatch(url, "*://*.metrics-vendor.net/*"))
    return "DIRECT";

  return "DIRECT";
}

Three things make a PAC worth the trouble. It expresses logic a condition table cannot: fallback chains in one return value, arithmetic, day of week, host arithmetic. It travels as one file, so a team of six gets the same routing from one URL. And it survives the extension being reinstalled, since the profile holds only a link.

Three things push back. A PAC carries no credentials at all, so every endpoint in it has to be reachable by bound address. dnsResolve and myIpAddress behave unevenly in the browser and I stopped calling them. And a syntax error takes the whole profile down silently, with the browser falling back to direct while the extension icon still shows the profile as active. I keep a copy of the script in a local editor and lint it before it goes up, because that silent failure looks exactly like a working setup with a broken pool.

For SOCKS the return string uses a different keyword, and getting it wrong is a common waste of an hour:

// HTTP endpoint
return "PROXY 203.0.113.41:8080";
// SOCKS5 endpoint, with the older keyword kept as fallback
return "SOCKS5 203.0.113.41:1080; SOCKS 203.0.113.41:1080; DIRECT";

Rule order, and how I find the condition that wins

Every conflict I have had came down to the same shape: two rows match the same request and the higher one takes it. The extension gives no warning about overlap, and the interface shows nothing special on either row. You find it by looking.

My working order, top to bottom, holds five bands. Local and internal names first, so nothing inside the network ever leaves the machine. Then explicit exceptions, the single hosts I want direct even though a broader rule below would grab them. Then narrow URL rules, the ones with a path in them. Then broad host rules. Then the default.

Put the bands the other way round and the broad rule swallows the narrow one. A row reading *.target-site.com placed above *://api.target-site.com/v3/* makes the second row unreachable forever, and the interface still draws it in black text as though it were live.

When a page behaves oddly, the sequence I run takes about four minutes. Open the popup while the page is loading and read the resource list at the bottom: it names each host the tab touched and the profile that handled it. Find the host that went the wrong way. Then walk the condition table from the top and stop at the first row that could match that host, remembering that a host wildcard also covers the bare domain. That row is the one deciding, whatever your intention was when you wrote the row lower down.

Two extra causes are worth checking before you rewrite the table. Another extension holding the browser proxy setting will override yours entirely, and Chrome then shows a controlled-by-another-extension notice in settings; the fix is to disable the other one, since two extensions cannot share that setting. And a rule list that failed its last refresh keeps serving the previous copy, so a domain you removed upstream can still be routing an hour later. The refresh timestamp sits next to the rule list URL in the profile, and I check it first on any conflict I cannot explain from the local table.

The habit that saved me the most time is trivial: a comment column. The extension gives every condition row a free-text field, and I write the reason there, four or five words. Six months later that field is the difference between a confident edit and a cautious one.

Moving a working configuration to a second machine

The Options page has an Import/Export section with three routes out. A backup file downloads the whole configuration as JSON. A PAC export writes out the current switch logic as a script, useful when a device accepts a PAC URL and nothing else. Online sync pushes the configuration through the browser account.

I use the file route on principle. Sync worked fine while my configuration stayed small, then started throwing quota errors as the rule count climbed, because the sync area holds roughly 100 KB per item and a large condition table plus a cached rule list crosses that line. The failure is quiet on one machine and loud on another, which makes it hard to trust.

The backup file is plain JSON and readable by hand. The part I edit most looks like this:

{
  "+work-exit": {
    "name": "work-exit",
    "profileType": "FixedProfile",
    "fallbackProxy": {
      "scheme": "http",
      "host": "203.0.113.41",
      "port": 8080
    },
    "bypassList": [
      { "conditionType": "BypassCondition", "pattern": "<local>" },
      { "conditionType": "BypassCondition", "pattern": "127.0.0.1/8" },
      { "conditionType": "BypassCondition", "pattern": "192.168.0.0/16" }
    ]
  }
}

Every profile key carries a + prefix, and the key has to match the name field or the import drops it without a message. When I move a configuration between machines I open the file, swap the host and port values for the endpoints bound to the target machine, and import. Passwords do not travel in the backup, so the lock icon needs filling in again on arrival, which takes a minute per server and is the correct behaviour for a file that lands in a downloads folder.

For the machines that only need reading access with no credential handling at all, I point the fixed row at anonymous proxy endpoints for browser sessions bound by address, and the same JSON file imports on any of them with a single host edit.

What the popup shows while you are debugging

The popup is the part people underuse. It carries four things worth reading.

At the top sits the profile currently applied to the browser, with the colour you assigned. The colour matters more than it sounds: I give the direct profile grey and every endpoint profile a strong colour, so a glance at the toolbar answers the question of whether I am about to log into an account through the wrong exit.

Below that is the profile list for a one-click switch, and under it the entry for a temporary rule scoped to the current tab. The temporary rule writes itself into the condition table at the top of the list, which is exactly where you want it during a test, and exactly where you must remember to remove it afterwards. I have twice left a temporary direct rule in place for a week.

The bottom section is the resource list, and it is the real instrument here. It shows the hosts the current tab requested and which profile handled each one, with an add-rule button beside every line. When a page half-loads through the endpoint and half-loads direct, this list tells you which half within seconds. The one caveat: it only fills while the tab is loading, so open the popup during the load or reload with the popup logic in mind.

The icon carries error state too. A failed PAC fetch or a rule list that cannot be reached shows up as a marker on the icon, and the details land in the extension error log. I check that log before touching any rule, because a broken fetch produces symptoms that look identical to a broken table.

What never passes through the extension

This is the section I wish I had read first. The extension writes the browser proxy configuration, so its reach ends at the edge of that browser profile. Several kinds of traffic sit outside that edge, and each one has produced a support ticket somewhere.

TrafficWhere it actually goesWhat I do about it
Other browsers and desktop appsStraight out on the machine addressRoute them at the OS layer with a separate tool
Requests from another Chrome user profileIts own proxy configuration, independent of yoursConfigure each browser profile separately
Incognito windowsTheir own proxy scope, which the extension needs permission to touchGrant incognito access in the extension entry
WebRTC peer connectionsCan reveal the local and public address over UDPSet the browser WebRTC handling policy by hand
QUIC and UDP transportsNo proxy carries UDP; the browser falls back to TCPConfirm the fallback happened during a test
DNS lookups on SOCKS4Resolved on the machine before the connection opensUse SOCKS5 with remote DNS enabled
chrome:// pages and local filesNever leave the browserNothing needed
Captive portal and connectivity checksSpecial-cased by the browserNothing needed

The DNS row is the one that costs people their careful setup. With an HTTP endpoint the browser sends the hostname in the CONNECT line and the endpoint resolves it, so nothing leaks. With SOCKS5 the extension has a checkbox for proxying DNS, and with it off the machine resolves the name locally before opening the tunnel. A resolver log on the machine side then holds a list of every host you visited. Turn the checkbox on and confirm it with a lookup test, since the default has changed between versions.

WebRTC deserves its own paragraph because it defeats the whole arrangement in one line of page script. A site can open a peer connection and read the candidate addresses, which include the local network address and, in some configurations, the public one, none of it passing through the endpoint. The browser has a policy setting for how it handles those candidates, and on machines where the browser identity has to hold up I set it to the most restrictive option and verify with a candidate dump. Where the work goes further than routing, into fingerprints and separate storage per identity, the browser extension hands over to an antidetect browser setup that carries its own profile isolation.

The multi-profile point catches teams. Chrome user profiles each hold their own proxy configuration, so installing the extension in one and switching it there leaves the others on the direct path. On a machine with four browser profiles I keep four copies of the same backup file imported, and I confirm the state in each with a page that echoes the requesting address.

The numbers I track before and after a change

I keep a short journal for the browser setup, one line per change, and it has paid for itself several times. Four values go in: date of the change, requests per page load, median response time on a reference page, and the count of requests that went out on the wrong path. That last column comes from the popup resource list, counted by hand on three page loads.

The reference page matters more than the numbers themselves. Mine is a product page on a site with 60 to 80 subresources across six hosts, which is enough breadth to expose a rule that grabs too much. On the direct profile it loads in about 900 ms and issues 74 requests. Through an HTTP endpoint in the same city band the same page came in at 1.3 seconds and 74 requests, so the routing added roughly 400 ms and dropped nothing.

The moment the journal earns its place is when a number moves for no visible reason. A jump from 74 to 79 requests told me a rule list refresh had added a domain that pulled an extra tracker. A median that went from 1.3 seconds to 4.1 told me a fallback chain in the PAC was trying a dead first entry and waiting for the timeout on every request. Neither of those is visible in the interface. Both are obvious in a four-column table.

One habit around endpoints, since the pool rotates. I test a fresh set against the reference page before I put it into the rules, three loads each, and keep anything under 2 seconds. The endpoints I keep for browser work come out of a pool of roughly 12 000 active addresses with automatic rotation inside it, so the test is quick and the result is a distribution I can plan around. Access sold by the month carries no cap on traffic, and the thread ceiling on the standard package sits at 1000, which a browser will never approach; a headless run with 40 workers alongside it will not approach it either.

For the machines where the browser talks plain HTTP with a login pair, I keep a separate row pointed at HTTP proxy access for browser sessions so a credential change touches one profile and nothing else in the table moves.

Everything above stops at the browser edge, so the next steps depend on what else runs on the machine. Routing desktop applications and other browsers is covered in the Proxifier guide for Windows, separating identities across many browser profiles at once is covered in the AdsPower profile guide, and the same endpoints in an automation context are covered in the Playwright proxy guide. When you need to turn a list entry into the exact string a given tool expects, the connection string builder does the conversion for HTTP, HTTPS and SOCKS5 forms without the transcription errors that come from doing it by hand.