Skip to content

Instantly share code, notes, and snippets.

@scpedicini
Forked from pudquick/brew.md
Created September 3, 2022 22:18
Show Gist options
  • Save scpedicini/a290a0f55b857778543e16f120013789 to your computer and use it in GitHub Desktop.
Save scpedicini/a290a0f55b857778543e16f120013789 to your computer and use it in GitHub Desktop.
Lightly "sandboxed" homebrew on macOS

brew is a bad neighbor

This isn't a guide about locking down homebrew so that it can't touch the rest of your system security-wise.

This guide doesn't fix the inherent security issues of a package management system that will literally yell at you if you try to do something about "huh, maybe it's not great my executables are writeable by my account without requiring authorization first".

But it absolutely is a guide about shoving it into its own little corner so that you can take it or leave it as you see fit, instead of just letting the project do what it likes like completely taking over permissions and ownership of a directory that might be in use by other software on your Mac and stomping all over their contents.

By following this guide you will:

  • Never have to run sudo to forcefully change permissions of some directory to be owned by your account
  • Pick and choose exactly what binaries from homebrew you want to have exposed to your environment
  • Make it possible to effectively "unhook" the installed homebrew environment on-demand
  • You could even have multiple separate installs side-by-side, if you wanted

optional beginnings

I create a lot of my own tools - and other times I'll have tools built by others that I just like to keep somewhere non-standard to avoid some app or something I install that relies on an opensource component that decides to ship those components in "their standard location" and ends up clobbering /usr/local or other paths.

So to that end, I created a .local folder in my home directory. The structure you'd find inside it is the same as you'd see in /usr or /usr/local, with a bin, lib, share, etc. I have this path prepended onto the front of my $PATH for my shell:

export PATH="~/.local/bin:$PATH"

The . at the beginning of .local ensures that if I'm looking in my home folder in the Finder, it won't clutter the view unless I reveal hidden files. You could choose any name you like if you want to do this. And because it's at the front of my $PATH, anything I link into .local/bin takes precedence - I know I'll be running a specific instance of a binary named foo if I put or link something named foo into it.

If you want a little extra security here, feel free to adjust the path permissions so that you can't write or add new things to this path without elevating. (But it won't be much, mind - homebrew will still be doing its thing as our unauthenticated user account in the end, as you'll see)

the real trick

homebrew (grudgingly?) supports being installed in a non-standard folder. They just don't suggest it to you up front. It's tucked away in the detailed installation page under "Alternative Installs":

https://docs.brew.sh/Installation#untar-anywhere

What's the downside of doing this?

  • A lot of precompiled packages will now require compiling, because it's not the standard path so the linked library paths in the prebuilt binaries won't work. As a concrete example: Installing the ghc package after freshly setting up homebrew as listed above required compilation - total time was 8 minutes and 7 seconds including all the dependencies.
  • Some (poorly built) packages potentially won't work because they make assumptions about path. In practice, I haven't run into this yet.

What's the upside of doing this?

  • It's not camped out in /usr/local or /opt
  • You're not blindly directly executing a script you just curl'd down
  • If you've got a managed IT environment that thinks massively distributing or managing a default path homebrew install across engineering laptops is a good idea, by doing this you won't be fighting with their install or having to run brew doctor to repair permissions flips
  • You never have to enter your password or use sudo to set it up
  • (And more we'll get into shortly here)

So I cd into my ~/.local and then follow the main instruction they have there:

mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew

That's it really. You've got homebrew installed now. Since it was installed in your home directory - no need to change any permissions. No sudo required.

But you still have to patch in brew itself to your shell session, since you've got it tucked away right now in ~/.local/homebrew/bin/brew, which so far you haven't added to your $PATH.

but don't just link it into or adjust your PATH

homebrew, as part of some of the other things it does that I don't like, if you symlink it into a bin-like path that you've included in your $PATH, when you execute it it will "helpfully" analyze the path it was called from and use that as a target destination for installing symlinks to all the binaries anything you install generates.

But we don't want that. We put baby in the corner and it needs to stay there.

So the trick is adding this to your shell environment: alias brew=~/.local/homebrew/bin/brew

This makes it so that brew is available to your shell environment as a callable command, but what it's calling is the fully qualified path that brew already lives in, as a subdirectory under ~/.local/homebrew

The net effect of this is: everything you install or update with brew stays in this folder

All of the binaries only get linked into ~/.local/homebrew/bin

Want to completely nuke this homebrew install? Remove the directory - done!

Done installing something with brew?

You can now use ln to symlink exactly what you please into ~/.local/bin. So if the act of installing a single package in homebrew results in 5+ additional packages being installed, all with their own binaries, when it's finished installing everything and the kitchen sink you can make sure that only the exact single thing that you wanted in the first place is allowed to be seen outside of that directory (instead of finding out later that something had a side effect of installing a brew copy of python you never wanted, etc).

This is why I said not to adjust your $PATH to include homebrew/bin itself to make brew work. It would work - but it would also make every single thing you installed exposed as callable in your shell environment. We don't want that.

Worried about other projects that use headers or libraries installed by brew?

Most of the ones I've seen are smart and use $(brew --prefix) to automatically figure out the correct platform independent path your libraries are at. Easily 95% or more of what I see out there is doing this. There are some things you'll run into out there though where that might not be the case, but you can fix that almost always by using something like:

CFLAGS="-I$(brew --prefix)/include" LDFLAGS="-L$(brew --prefix)/lib"

want to unlink homebrew because you're trying to build something else?

It's easy. Just get rid of the alias. alias brew=

Now its existence isn't discoverable. All of your built packages are off in their ~/.local/homebrew jail, but because it's on a non-standard path nothing is able to discover and link to the results - and because brew itself can't be found, it can't be queried for library/linker paths.

There's the danger here still of them finding or tripping across the binaries you've linked into ~/.local/bin, sure. But if you think you might want to take advantage of the ability to hide brew products during a build, you could always keep that in a separate brewbin path that you add or remove from your $PATH.

Now that you understand the basics of how this works, there's lots of options and flexibility. Maybe try to fix some of the core security issues in homebrew and move its path and managing it to a dedicated service account. Do what you like - go wild!

But most importantly: don't let brew win

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment