mac comments edit

Scenario: You’re installing something from Homebrew and, for whatever reason, that standard formula isn’t working for you. What do you do?

I used this opportunity to learn a little about how Homebrew formulae generally work. It wasn’t something where I had my own app to deploy, but it also wasn’t something I wanted to submit as a PR for an existing formula. For example, I wanted to have the bash and wget formulae use a different main URL (one of the mirrors). The current one works for 99% of folks, but for reasons I won’t get into, it wasn’t working for me.

This process is called “creating a tap” - it’s a repo you’ll own with your own stuff that won’t go into the core Homebrew repo.


  • Create a GitHub repo called homebrew-XXXX where XXXX is how Homebrew will see your repo name.
  • Copy the original formulae into your repo. Anything with a .rb extension will work - the name of the file is the name of the formula.
  • Install using brew install your-username/XXXX/formula.rb

Let’s get a little more specific and use an example.

First I created my GitHub repo, homebrew-mods. This is where I can store my customized formulae. In there, I created a Formula folder to put them in.

I went to the homebrew-core repo where all the main formulae are and found the ones I was interested in updating:

I copied the formulae into my own repo and made some minor updates to switch the url and mirror values around a bit.

Finally, install time! It has to be installed in this order because otherwise the dependencies in the bash and wget modules will try to pull from homebrew-core instead of my mod repo.

brew install tillig/mods/gettext
brew install tillig/mods/bash
brew install tillig/mods/libidn2
brew install tillig/mods/wget

That’s it! If other packages have dependencies on gettext or libidn2, it’ll appear to be already installed since Homebrew just matches on name.

The downside of this approach is that you won’t get the upgrades for free. You have to maintain your tap and pull version updates as needed.

If you want to read more, check out the documentation from Homebrew on creating and maintaining a tap as well as the formula cookbook.

mac comments edit

Here’s the proposition: You just got a new Mac and you need to set it up for development on Azure (or your favorite cloud provider) in multiple different languages and technologies but you don’t have any admin permissions at all. Not even a little bit. How do you get it done? We’re going to give it a shot.

BIGGEST DISCLAIMER YOU HAVE EVER SEEN: THIS IS UNSUPPORTED. Not just “unsupported by me” but, in a lot of cases, unsupported by the community. For example, we’ll be installing Homebrew in a custom location, and they have no end of warnings about how unsupported that is. They won’t even take tickets or PRs to fix it if something isn’t working. When you take this on, you need to be ready to do some troubleshooting, potentially at a level you’ve not had to dig down to before. Don’t post questions, don’t file issues - you are on your own, 100%, no exceptions.

OK, hopefully that was clear. Let’s begin.

The key difference in what I’m doing here is that everything goes into your user folder somewhere.

  • You don’t have admin, so you can’t install Homebrew in its default /usr/local/bin style location.
  • You don’t have admin, so you can’t use most standard installers that want to put apps in central locations like /Applications or /usr/share.
  • You don’t have admin, so you can’t modify /etc/paths.d or anything like that.



The TL;DR here is a set of strategies:

  • Install to your user folder.
    • Instead of /usr/local/bin or anything else under /usr/local, we’re going to create that whole structure under ~/local - ~/local/bin and so on.
    • Applications will go in ~/Applications instead of /Applications.
  • Use your user ~/.profile for paths and environment. No need for /etc/paths.d. Also, ~/.profile is pretty cross-shell (e.g., both bash and pwsh obey it) so it’s a good central way to go.
  • Use SDK-based tools instead of global installers. Look for tools that you can install with, say, npm install -g or dotnet tool install -g if you can’t find something in Homebrew.

Start with Git

First things first, you need Git. This is the only thing that you may have challenges with. Without admin I was able to install Xcode from the App Store and that got me git. I admit I forgot to even check to see if git just ships with MacOS now. Maybe it does. But you will need Xcode command line tools for some stuff with Homebrew anyway, so I’d say just install Xcode to start. If you can’t… hmmm. You might be stuck. You should at least see what you can do about getting git. You’ll only use this version temporarily until you can install the latest using Homebrew later.


Got Git? Good. Let’s get Homebrew installed.

mkdir -p ~/local/bin
cd ~/local
git clone Homebrew
ln -s ~/local/Homebrew/bin/brew ~/local/bin/brew

I’ll reiterate - and you’ll see it if you ever run brew doctor - that this is wildly unsupported. It works, but you’re going to see some things here that you wouldn’t normally see with a standard Homebrew install. For example, things seem to compile a lot more often than I remember with regular Homebrew - and this is something they mention in the docs, too.

Now we need to add some stuff to your ~/.profile so we can get the shell finding your new ~/local tools. We need to do that before we install more stuff via Homebrew. That means we need an editor. I know you could use vi or something, but I’m a VS Code guy, and I need that installed anyway.

VS Code

Let’s get VS Code. Go download it from the download page, unzip it, and drop it in your ~/Applications folder. At a command prompt, link it into your ~/local/bin folder:

ln -s '~/Applications/Visual Studio' ~/local/bin/code

I was able to download this one with a browser without running into Gatekeeper trouble. If you get Gatekeeper arguing with you about it, use curl to download.

Homebrew Profile

You can now do ~/local/bin/code ~/.profile to edit your base shell profile. Add this line so Homebrew can put itself into the path and set various environment variables:

eval "$($HOME/local/bin/brew shellenv)"

Restart your shell so this will evaluate and you now should be able to do:

brew --version

Your custom Homebrew should be in the path and you should see the version of Homebrew installed. We’re in business!

Homebrew Formulae

We can install more Homebrew tools now that custom Homebrew is set up. Here are the tools I use and the rough order I set them up. Homebrew is really good about managing the dependencies so it doesn’t have to be in this order, but be aware that a long dependency chain can mean a lot of time spent doing some custom builds during the install and this general order keeps it relatively short.

# Foundational utilities
brew install ca-certificates
brew install grep
brew install jq

# Bash and wget updates
brew install gettext
brew install bash
brew install libidn2
brew install wget

# Terraform - I use tfenv to manage installs/versions. This will
# install the latest Terraform.
brew install tfenv
tfenv install

# Terrragrunt - I use tgenv to manage installs/versions. After you do
# `list-remote`, pick a version to install.
brew install tgenv
tgenv list-remote

# Go
brew install go

# Python
brew install python@3.10

# Kubernetes
brew install kubernetes-cli
brew install k9s
brew install krew
brew install Azure/kubelogin/kubelogin
brew install stern
brew install helm
brew install helmsman

# Additional utilities I like
brew install marp-cli
brew install mkcert
brew install pre-commit

If you installed the grep update or python, you’ll need to add them to your path manually via the ~/.profile. We’ll do that just before the Homebrew part, then restart the shell to pick up the changes.

export PATH="$HOME/local/opt/grep/libexec/gnubin:$HOME/local/opt/python@3.10/libexec/bin:$PATH"
eval "$($HOME/local/bin/brew shellenv)"


This one was more challenging because the default installer they provide requires admin permissions so you can’t just download and run it or install via Homebrew. But I’m a PowerShell guy, so here’s how that one worked:

First, find the URL for the the .tar.gz from the releases page for your preferred PowerShell version and Mac architecture. I’m on an M1 so I’ll get the arm64 version.

cd ~/Downloads
curl -fsSL -o powershell.tar.gz
mkdir -p ~/local/microsoft/powershell/7
tar -xvf ./powershell.tar.gz -C ~/local/microsoft/powershell/7
chmod +x ~/local/microsoft/powershell/7/pwsh
ln -s '~/local/microsoft/powershell/7/pwsh' ~/local/bin/pwsh

Now you have a local copy of PowerShell and it’s linked into your path.

An important note here - I used curl instead of my browser to download the .tar.gz file. I did that to avoid Gatekeeper.

Azure CLI and Extensions

You use Homebrew to install the Azure CLI and then use az itself to add extensions. I separated this one out from the other Homebrew tools, though, because there’s a tiny catch: When you install az CLI, it’s going to build openssl from scratch because you’re in a non-standard location. During the tests for that build, it may try to start listening to network traffic. If you don’t have rights to allow that test to run, just hit cancel/deny. It’ll still work.

brew install azure-cli
az extension add --name azure-devops
az extension add --name azure-firewall
az extension add --name fleet
az extension add --name front-door

Node.js and Tools

I use n to manage my Node versions/installs. n requires us to set an environment variable N_PREFIX so it knows where to install things. First install n via Homebrew:

brew install n

Now edit your ~/.profile and add the N_PREFIX variable, then restart your shell.

export N_PREFIX="$HOME/local"
export PATH="$HOME/local/opt/grep/libexec/gnubin:$HOME/local/opt/python@3.10/libexec/bin:$PATH"
eval "$($HOME/local/bin/brew shellenv)"

After that shell restart, you can start installing Node versions. This will install the latest:

n latest

Once you have Node.js installed, you can install Node.js-based tooling.

# These are just tools I use; install the ones you use.
npm install -g @stoplight/spectral-cli `
               gulp-cli `
               tfx-cli `


I use rbenv to manage my Ruby versions/installs. rbenv requires both an installation and a modification to your ~/.profile. If you use rbenv

# Install it, and install a Ruby version.
brew install rbenv
rbenv init
rbenv install -l

Update your ~/.profile to include the rbenv shell initialization code. It’ll look like this, put just after the Homebrew bit. Note I have pwsh in there as my shell of choice - put your own shell in there (bash, zsh, etc.). Restart your shell when it’s done.

export N_PREFIX="$HOME/local"
export PATH="$HOME/local/opt/grep/libexec/gnubin:$HOME/local/opt/python@3.10/libexec/bin:$PATH"
eval "$($HOME/local/bin/brew shellenv)"
eval "$($HOME/local/bin/rbenv init - pwsh)"

.NET SDK and Tools

The standard installers for the .NET SDK require admin permissions because they want to go into /usr/local/share/dotnet.

Download the shell script and stick that in your ~/local/bin folder. What’s nice about this script is it will install things to ~/.dotnet by default instead of the central share location.

# Get the install script
curl -fsSL -o ~/local/bin/
chmod +x ~/local/bin/

# And the .NET uninstall tool (
curl -fsSL -o ~/Downloads/dotnet-core-uninstall.tar.gz
tar -xvf ~/Downloads/dotnet-core-uninstall.tar.gz -C ~/local/bin
chmod +x ~/local/bin/dotnet-core-uninstall

We need to get the local .NET into the path and set up variables (DOTNET_INSTALL_DIR and DOTNET_ROOT) so .NET and the install/uninstall processes can find things. We’ll add that all to our ~/.profile and restart the shell.

export DOTNET_INSTALL_DIR="$HOME/.dotnet"
export DOTNET_ROOT="$HOME/.dotnet"
export N_PREFIX="$HOME/local"
export PATH="$HOME/local/opt/grep/libexec/gnubin:$DOTNET_ROOT:$DOTNET_ROOT/tools:$HOME/local/opt/python@3.10/libexec/bin:$PATH"
eval "$($HOME/local/bin/brew shellenv)"
eval "$($HOME/local/bin/rbenv init - pwsh)"

Note we did not grab the .NET uninstall tool. It doesn’t work without admin permissions. When you try to run it doing anything but listing what’s installed, you get:

The current user does not have adequate privileges. See

It’s unclear why uninstall would require admin privileges since install did not. I’ve filed an issue about that.

After the shell restart, we can start installing .NET and .NET global tools. In particular, this is how I get the Git Credential Manager plugin.

# Install latest .NET 6.0, 7.0, 8.0 -? -c 6.0 -c 7.0 -c 8.0

# Get Git Credential Manager set up.
dotnet tool install -g git-credential-manager
git-credential-manager configure

# Other .NET tools I use. You may or may not want these.
dotnet tool install -g dotnet-counters
dotnet tool install -g dotnet-depends
dotnet tool install -g dotnet-dump
dotnet tool install -g dotnet-format
dotnet tool install -g dotnet-guid
dotnet tool install -g dotnet-outdated-tool
dotnet tool install -g dotnet-script
dotnet tool install -g dotnet-svcutil
dotnet tool install -g dotnet-symbol
dotnet tool install -g dotnet-trace
dotnet tool install -g gti
dotnet tool install -g microsoft.web.librarymanager.cli


Without admin, you can’t get the system Java wrappers to be able to find any custom Java you install because you can’t run the required command like: sudo ln -sfn ~/local/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

If you use bash or zsh as your shell, you might be interested in SDKMAN! as a way to manage Java. I use PowerShell so this won’t work because SDKMAN! relies on shell functions to do a lot of its job.

Instead, we’ll install the appropriate JDK and set symlinks/environment variables.

brew install openjdk

In .profile, we’ll need to set JAVA_HOME and add OpenJDK to the path. If we install a different JDK, we can update JAVA_HOME and restart the shell to switch.

export DOTNET_INSTALL_DIR="$HOME/.dotnet"
export DOTNET_ROOT="$HOME/.dotnet"
export N_PREFIX="$HOME/local"
export JAVA_HOME="$HOME/local/opt/openjdk"
export PATH="$JAVA_HOME/bin:$HOME/local/opt/grep/libexec/gnubin:$DOTNET_ROOT:$DOTNET_ROOT/tools:$HOME/local/opt/python@3.10/libexec/bin:$PATH"
eval "$($HOME/local/bin/brew shellenv)"
eval "$($HOME/local/bin/rbenv init - pwsh)"

Azure DevOps Artifacts Credential Provider

If you use Azure DevOps Artifacts, the credential provider is required for NuGet to restore packages. There’s a script that will help you download and install it in the right spot, and it doesn’t require admin.

wget -qO- | bash

Issue: Gatekeeper

If you download things to install, be aware Gatekeeper may get in the way.

Gatekeeper can't scan this package

You get messages like “XYZ can’t be opened because Apple cannot check it for malicious software.” This happened when I tried to install PowerShell by downloading the .tar.gz using my browser. The browser adds an attribute to the downloaded file and prompts you before running it. Normally you can just approve it and move on, but I don’t have permissions for that.

To fix it, you have to use the xattr tool to remove the attribute from the affected file(s).

xattr -d myfile.tar.gz

An easier way to deal with it is to just don’t download things with a browser. If you use curl to download, you don’t get the attribute added and you won’t get prompted.

Issue: Admin-Only Installers

Some packages installed by Homebrew (like PowerShell) try to run an installer that requires admin permissions. In some cases you may be able to find a different way to install the tool like I did with PowerShell. In some cases, like Docker, you need the admin permissions to set that up. I don’t have workarounds for those sorts of things.

Issue: App Permissions

There are some tools that may require additional permissions by nature, like Rectangle needs to be allowed to control window placement and I don’t have permissions to grant that. I don’t have workarounds for those sorts of things.

Issue: Bash Completions

Some Homebrew installs will dump completions into ~/local/etc/bash_completions.d. I never really did figure out what to do about these since I don’t really use Bash. There’s some doc about options you have but I’m not going to dig into it.


Hopefully this gets you bootstrapped into a dev machine without requiring admin permissions. I didn’t cover every tool out there, but perhaps you can apply the strategies to solving any issues you run across. Good luck!

javascript, git comments edit

I was recently introduced to pre-commit, and I really dig it. It’s a great way to double-check basic linting and validity in things without having to run a full build/test cycle.

Something I commonly do is sort JSON files using json-stable-stringify. I even wrote a VS Code extension to do just that. The problem with it being locked in the VS Code extension is that it’s not something I can use to verify formatting or invoke outside of the editor, so I set out to fix that. The result: @tillig/json-sort-cli.

This is a command-line wrapper around json-stable-stringify which adds a couple of features:

  • It obeys .editorconfig - which is also something the VS Code plugin does.
  • It can warn when something isn’t formatted (the default behavior) or autofix it if you want.
  • It supports JSON with comments (using json5 for parsing) but it will remove those comments on format.

I put all of that together and included configuration for pre-commit so you can either run it manually via CLI or have it automatically run at pre-commit time.

I do realize there is already a pretty-format-json hook, but the above features I mentioned are differentiators. Why not just submit PRs to enhance the existing hook? The existing hook is in Python (not a language I’m super familiar with) and I really wanted - explicitly - the json-stable-stringify algorithm here, which I didn’t want to have to re-create in Python. I also wanted to add .editorconfig support and ability to use json5 to parse, which I suppose is all technically possible in Python but not a hill I really wanted to climb. Also, I wanted to offer a standalone CLI, which isn’t something I can do with that hook.

This is my first real npm package I’ve published, and I did it without TypeScript (I’m not really a JS guy, but to work with pre-commit you need to be able to install right from the repo), so I’m pretty pleased with it. I learned a lot about stuff I haven’t really dug into in the past - from some new things around npm packaging to how to get GitHub Actions to publish the package (with provenance) on release.

If this sounds like something you’re into, go check out how you can install and start using it!

git, github, powershell comments edit

The GitLens plugin for VS Code is pretty awesome, and I find I use the “Open Repository on Remote” function to open the web view in the system browser is something I use a lot.

Open Repository on Remote - GitLens

I also find that I do a lot of my work at the command line (in PowerShell!) and I was missing a command that would do the same thing from there.

Luckily, the code that does the work in the GitLens plugin is MIT License so I dug in and converted the general logic into a PowerShell command.

# Open the current clone's `origin` in web view.

# Specify the location of the clone.
Open-GitRemote ~/dev/my-clone

# Pick a different remote.
Open-GitRemote -Remote upstream

If you’re interested, I’ve added the cmdlet to my PowerShell profile repository which is also under MIT License, so go get it!

Note: At the time of this writing I only have Windows and MacOS support - I didn’t get the Linux support in, but I think xdg-open is probably the way to go there. I just can’t test it. PRs welcome!

halloween, maker, costumes comments edit

Due to some challenges with home remodeling issues we didn’t end up handing out candy this year.

We discovered a slow leak in one of the walls in our kitchen that caused some of our hardwood floor to warp, maybe a little more than a square meter. Since this was a very slow leak over time, insurance couldn’t say “here’s the event that caused it” and, thus, chalked it up to “normal wear and tear” which isn’t covered.

You can’t fix just a small section of a hardwood floor and we’ve got like 800 square feet of contiguous hardwood, so… all 800 square feet needed to be fully sanded and refinished. All out of pocket. We packed the entire first floor of the house into the garage and took a much-needed vacation to Universal Studios California and Disneyland for a week while the floor was getting refinished.

I had planned on putting the house back together, decorating, and getting right into Halloween when we came back. Unfortunately, when we got back we saw the floor was not done too well. Lots of flaws and issues in the work. It’s getting fixed, but it means we didn’t get to empty out the garage, which means I couldn’t get to the Halloween decorations. Between work and stress and everything else… candy just wasn’t in the cards. Sorry kids. Next year.

But we did make costumes - and we wore them in 90 degree heat in California for the Disney “Oogie Boogie Bash” party. So hot, but still very fun.

I used this Julie-Chantal pattern for a Jedi costume and it is really good. I’m decent at working with and customizing patterns, I’m not so great with drafting things from scratch.

I used a cotton gauze for the tunic, tabard, and sash. The robe is a heavy-weave upholstery fabric that has a really nice feel to it.

Texture of the robe fabric up close

I added some magnet closures to it so it would stick together a bit nicer as well as some snaps to stick things in place. I definitely found while wearing it that it was required. All the belts and everything have a tendency to move a lot as you walk, sit, and stand. I think it turned out nicely, though.

The Jedi costume on a dress form

The whole family went in Star Wars garb. I don’t have a picture of Phoenix, but here’s me and Jenn at a Halloween party. Phoenix and Jenn were both Rey, but from different movies. You can’t really tell, but Jenn’s vest is also upholstery fabric with an amazing, rich texture. She did a great job on her costume, too.

Trav and Jenn in Star Wars costumes