HostSwitcher

Switch /etc/hosts environments from the menu bar

Named sets of entries, one click to switch, and the name cache flushed in the same breath. No sudo nano. No 150 MB of Electron.

Download for macOS Free and open source · macOS 26+ · Apple silicon
Signed, but not notarized. macOS refuses a downloaded copy without a word — no window, no error, nothing at all. Clear the quarantine attribute before the first launch: it is three commands, and the order matters more than the commands do.
The HostSwitcher menu bar panel: four sets — Home, Development, Staging, Production — each with a checkbox, and their entries beneath them.
The menu bar panel. A click on a set, and the file changes.

Why editing /etc/hosts by hand gets old

Developers switch domains between environments several times a day: the same api.example.com has to point now at production, now at staging, now at a local container. Doing that through sudo nano /etc/hosts is slow and error-prone, and after every edit you still have to remember to flush the name cache.

The existing solutions either cap the number of entries, or weigh 150 MB, or have not been updated in a long time. The niche HostSwitcher fills is native, light, and without limits.

Sets of hosts, and entries you switch one at a time

A set is a named group of entries. Every entry is switched on separately; a set is switched off as a whole. Order is preserved, the count is not limited, and every set you have is on screen at once — no picking one from a list to find out what is in it.

The toggle shows the state of the file, not an intention: if applying fails, it goes back.

The editor window: four sets as sections of one list, each entry a row of address, host names and a note.
Every set and every entry, in one list.

It does not touch the rest of your hosts file

Everything outside the managed block is preserved byte for byte: spaces, tabs, the line-break style, the presence of a final newline. A backup is written before every change, and an untouchable snapshot is kept of the file as it was before the very first one.

A disabled entry is not deleted. It stays in the file, commented out and labelled, so the file always shows the whole picture:

# >>> host-switcher managed block — do not edit >>>
# >>> host-switcher node: Work
127.0.0.1 api.example.com
# host-switcher off: 10.0.0.1 db.example.com
# <<< host-switcher <<<

It refuses to make things worse

An invalid line rejects the whole apply, naming the line number and the reason. One host name pointing at two different addresses in two enabled sets blocks applying as well — the resolver would have silently picked one of them, and which one is not something you get to know.

The editor refusing to apply: one host name claimed by two enabled sets, with the reason on each row and a summary at the foot of the window.
One name, two sets, two addresses. Applying is blocked until you decide.

Switch hosts from a script, without a password

hostswitcher on Staging switches a set and applies it. status names every set and what the system is actually using — green for what is in effect, red for what is not, and no colour at all when the output is a pipe.

$ hostswitcher status
Work                         on
  127.0.0.1 api.example.com  on
  10.0.0.1  db.example.com   off
Staging                      off
  127.0.0.1 api.staging      off

Every outcome has an exit code a script can act on, including a separate one for “this machine was never set up”, which is a different problem from a request that was refused. Reading needs no privileges and works with the app closed; switching needs no password. Asking twice is not an error, so a Makefile target can use it.

It updates itself, and says so before it does

Once a day the app asks GitHub whether a newer release has been published. If there is one, the panel gains a single row; press it and the product fetches that release, checks it, replaces itself and comes back at the new version — no browser, no download, no Gatekeeper refusal.

The check is one anonymous request carrying nothing about you or your sets, and a switch in the settings stops the product going out on its own, leaving a button.

The settings window in two columns: launch at login, interface language, hotkey and command line on the left; updates and a disable-all action on the right.
The settings page says what the product does on its own, and lets you stop it.

Installing it, the first time

The package is signed, but not notarized — the certificate behind it is a development one, not a Developer ID. macOS therefore refuses to start it as downloaded, and the refusal is not a message: nothing happens at all.

xattr -dr com.apple.quarantine ~/Downloads/HostSwitcher.zip
ditto -x -k ~/Downloads/HostSwitcher.zip /Applications
open /Applications/HostSwitcher.app

Clear the attribute before you open it, not after. macOS records a refusal against the path: once a copy at that path has been refused, nothing placed there starts afterwards — not the same copy with the attribute cleared, not a reinstall, not even a correctly signed build. Restarting the machine clears the record; so does installing under a different path.

The app has no icon in the Dock — it lives in the menu bar. Approve the background item when macOS asks, and it can apply changes.

This is the last installation you do by hand. From here the product finds newer versions itself: what it downloads never carries the quarantine attribute, so nothing refuses it.

What it does not do

  • It is not notarized yet. Until there is a Developer ID behind it, the first launch takes the three commands above.
  • There is no undo. Editing is direct; a deleted entry is gone, though the app asks before deleting a set or an entry.
  • Sets are a flat list. No groups, no nesting, no exclusive mode.
  • macOS 26 or newer, Apple silicon. The privileged helper uses a registration API that older systems do not have.
  • Uninstalling leaves one trace that is not ours to remove. The system's record of background items survives; nothing of ours runs, but the entry stays visible in System Settings. macOS offers no way to delete a single record.