azure comments edit

At work I use the az CLI behind a VPN/proxy package called Zscaler. I’m not a big fan of these TLS-intercepting-man-in-the-middle-attack sort of “security” products, but it is what it is.

The problem for me is that, if I move to a new machine, or if someone else is setting up a machine, I always forget how to make the az CLI trust Zscaler so it can function properly and not get a TLS certificate error. I’ve re-figured this out countless times, so this time I’m writing it down. It does seem to be slightly different on Mac and Windows and I’m not sure why. Perhaps it has to do with the different ways the network stack works or something.

The az CLI is Python-based so this will ostensibly work to generally solve Python issues, but I always encounter it as part of az, so I’m blogging it as such.

Zscaler does have some help for enabling trust but you sometimes have to fudge the steps, like with this.

On Mac

I’m not sure why this works. That bundle doesn’t have the Zscaler certificate in it, but without doing this things get TLS errors.

On Windows

  • Go get the latest ca-certificates bundle from here.
  • Open that cert.pem file in your favorite text editor. Just make sure you keep the file with LF line endings.
  • Get your Zscaler CA certificate in PEM format. Open that up in the text editor, too.
  • At the bottom of the cert.pem main file, paste in the Zscaler CA certificate contents, thereby adding it to the list of CAs.
  • Set the REQUESTS_CA_BUNDLE environment variable to point at the cert.pem that has all the CA certs in it.

Again, not sure why on Windows you need to have the Zscaler cert added to the main cert bundle but on Mac you don’t. This also could just be something environmental - like there’s something on my work machines that somehow auto-trusts Zscaler but does so to the exclusion of all else.

Regardless, this is what worked for me.

architecture, mac, dotnet, ndepend comments edit

It’s been a few years since I’ve posted a look at NDepend and a lot has changed for me since then. I’ve switched my primary development machine to a Mac. I don’t use Visual Studio at all - I’m a VS Code person now because I do a lot of things in a lot of different languages and switching IDEs all day is painful (not to mention VS Code starts up far faster and feels far slimmer than full VS). Most of my day-to-day is in microservices now rather than larger monolith projects.

Full disclosure: Patrick at NDepend gave me the license for testing and showing off NDepend for free. I’m not compensated for the blog post in any way other than the license, but I figured it’d be fair to mention I was given the license.

I’ve loved NDepend from the start. I’ve been using it for years and it’s never been anything but awesome. I still think if you haven’t dived into that, you should just stop here and go do that because it’s worth it.

Get Going on Mac

The main NDepend GUI is Windows-only, so this time around, since I’m focusing solely on Mac support (that’s what I have to work with!) I’m going to wire this thing up and see how it goes.

First thing I need to do is register my license using the cross-platform console app. You’ll see that in the net8.0 folder of the zip package you get when you download NDepend.

dotnet ./net8.0/NDepend.Console.MultiOS.dll --reglic XXXXXXXX

This gives me a message that tells me my computer is now registered to run NDepend console.

Running the command line now, I get a bunch of options.

 pwsh> dotnet ./net8.0/NDepend.Console.MultiOS.dll
//  NDepend v2023.2.3.9706
//  Copyright (C) ZEN PROGRAM HLD 2004-2023
//  All Rights Reserved
To analyze code and build reports NDepend.Console.MultiOS.dll accepts these arguments.
NDepend.Console.MultiOS.dll can also be used to create projects (see how below after
the list of arguments).

  The path to the input .ndproj (or .xml) NDepend project file.  MANDATORY

    It must be specified as the first argument. If you need to specify a path
    that contains a space character use double quotes ".. ..". The specified
    path must be either an absolute path (with drive letter C:\ or
    UNC \\Server\Share format on Windows or like /var/dir on Linux or OSX),
    or a path relative to the current directory (obtained with
    or a file name in the current directory.

Following arguments are OPTIONAL and can be provided in any order. Any file or
directory path specified in optionals arguments can be:
  - Absolute : with drive letter C:\ or UNC \\Server\Share format on Windows
               or like /var/dir on Linux or OSX.
  - Relative : to the NDepend project file location.
  - Prefixed with an environment variable with the syntax  %ENVVAR%\Dir\
  - Prefixed with a path variable with the syntax   $(Variable)\Dir
  /ViewReport                 to view the HTML report
  /Silent                     to disable output on console
  /HideConsole                to hide the console window
  /Concurrent                 to parallelize analysis execution
  /LogTrendMetrics            to force log trend metrics
  /TrendStoreDir              to override the trend store directory specified
                              in the NDepend project file
  /PersistHistoricAnalysisResult   to force persist historic analysis result
  /DontPersistHistoricAnalysisResult   to force not persist historic analysis
  /ForceReturnZeroExitCode    to force return a zero exit code even when
                              one or many quality gate(s) fail
  /HistoricAnalysisResultsDir to override the historic analysis results
                              directory specified in the NDepend project file.
  /OutDir dir                 to override the output directory specified
                              in the NDepend project file.

     VisualNDepend.exe won't work on the machine where you used
     NDepend.Console.MultiOS.dll with the option /OutDir because VisualNDepend.exe is
     not aware of the output dir specified and will try to use the output dir
     specified in your NDepend project file.
  /AnalysisResultId id        to assign an identifier to the analysis result
  /GitHubPAT pat              to provide a GitHub PAT (Personal Access Token).

     Such PAT is used in case some artifacts (like a baseline analysis result) are
     required during analysis and must be loaded from GitHub.
     Such PAT overrides the PAT registered on the machine (if any).
  /XslForReport xlsFilePath   to provide your own Xsl file used to build report
  /KeepXmlFilesUsedToBuildReport  to keep xml files used to build report
  /InDirs [/KeepProjectInDirs] dir1 [dir2 ...]
                              to override input directories specified in the
                              NDepend project file.

     This option is used to customize the location(s) where assemblies to
     analyze (application assemblies and third-party assemblies) can be found.
     Only assemblies resolved in dirs are concerned, not assemblies resolved
     from a Visual Studio solution.
     The search in dirs is not recursive, it doesn't look into child dirs.
     Directly after the option /InDirs, the option /KeepProjectInDirs can be
     used to avoid ignoring directories specified in the NDepend
     project file.
  /CoverageFiles [/KeepProjectCoverageFiles] file1 [file2 ...]
                              to override input coverage files specified
                              in the NDepend project file.

     Directly after the option /CoverageFiles, the option
     /KeepProjectCoverageFiles can be used to avoid ignoring coverage files
     specified in the NDepend project file.

  /CoverageDir dir            to override the directory that contains
                              coverage files specified in the project file.

  /CoverageExclusionFile file to override the  .runsettings  file specified
                              in the project file. NDepend gathers coverage
                              exclusion data from such file.

  /RuleFiles [/KeepProjectRuleFiles] file1 [file2 ...]
                              to override input rule files specified
                              in the NDepend project file.

     Directly after the option /RuleFiles, the option
     /KeepProjectRuleFiles can be used to avoid ignoring rule files
     specified in the NDepend project file.
  /PathVariables Name1 Value1 [Name2 Value2 ...]
                              to override the values of one or several
                              NDepend project path variables, or
                              create new path variables.
  /AnalysisResultToCompareWith to provide a previous analysis result to
                               compare with.

     Analysis results are stored in files with file name prefix
     {NDependAnalysisResult_} and with extension {.ndar}.
     These files can be found under the NDepend project output directory.
     The preferred option to provide a previous analysis result to
     compare with during an analysis is to use:
     NDepend > Project Properties > Analysis > Baseline for Comparison
     You can use the option /AnalysisResultToCompareWith in special
     scenarios where using Project Properties doesn't work.

  /Help   or   /h              to display the current help on console

  Code queries execution time-out value used through NDepend.Console.MultiOS.dll

     If you need to adjust this time-out value, just run VisualNDepend.exe once
     on the machine running NDepend.Console.exe and choose a time-out value in:
        VisualNDepend > Tools > Options > Code Query > Query Execution Time-Out

     This value is persisted in the file VisualNDependOptions.xml that can be
     found in the directory:
        VisualNDepend > Tools > Options > Export / Import / Reset Options >
        Open the folder containing the Options File

NDepend.Console.MultiOS.dll can be used to create an NDepend project file.
This is useful to create NDepend project(s) on-the-fly from a script.

To do so the first argument must be  /CreateProject  or  /cp  (case-insensitive)

The second argument must be the project file path to create. The file name must
have the extension .ndproj. If you need to specify a path that contains a space
character use double quotes "...". The specified path must be either an
absolute path (with drive letter C:\ or UNC \\Server\Share format on Windows
or like /var/dir on Linux or OSX), or a path relative to the current directory
(obtained with System.Environment.CurrentDirectory),
or a file name in the current directory.

Then at least one or several sources of code to analyze must be precised.
A source of code to analyze can be:

- A path to a Visual Studio solution file.
  The solution file extension must be .sln.
  The vertical line character '|' can follow the path to declare a filter on
  project names. If no filter is precised the default filter "-test"
  is defined. If you need to specify a path or a filter that contains a space
  character use double quotes "...".
  Example: "..\My File\MySolution.sln|filterIn -filterOut".

- A path to a Visual Studio project file. The project file extension must
  be within: .csproj .vbproj .proj

- A path to a compiled assembly file. The compiled assembly file extension must
  be within: .dll .exe .winmd

Notice that source of code paths can be absolute or relative to the project file
location. If you need to specify a path or a filter that contains a space
character use double quotes.

NDepend.Console.MultiOS.dll can be used to register a license on a machine,
or to start evaluation. Here are console arguments to use (case insensitive):

  /RegEval      Start the NDepend 14 days evaluation on the current machine.

  /RegLic XYZ   Register a seat of the license key XYZ on the current machine.

  /UnregLic     Unregister a seat of the license key already registered
                on the current machine.
  /RefreshLic   Refresh the license data already registered on the current
                machine. This is useful when the license changes upon renewal.

Each of these operation requires internet access to do a roundtrip with the
NDepend server. If the current machine doesn't have internet access
a procedure is proposed to complete the operation by accessing manually the
NDepend server from a connected machine.

Register a GitHub PAT with NDepend.Console.MultiOS.dll

A GitHub PAT (Personal Access Token) can be registered on a machine.
This way when NDepend needs to access GitHub, it can use such PAT.
Here are console arguments to use (case insensitive):

  /RegGitHubPAT XYZ  Register the GitHub PAT XYZ on the current machine.

  /UnregGitHubPAT    Unregister the GitHub PAT actually registered on the
                     current machine.

As explained above, when using NDepend.Console.MultiOS.dll to run an analysis,
a PAT can be provided with the switch GitHubPAT.
In such case, during analysis the PAT provided overrides the PAT
registered on the machine (if any).

As usual, a great amount of help and docs right there to help me get going.

Create a Project

I created the project for one of my microservices by pointing at the microservice solution file. (Despite not using Visual Studio myself, some of our devs do, so we maintain compatibility with both VS and VS Code. Plus, the C# Dev Kit really likes it when you have a solution.)

dotnet ~/ndepend/net8.0/NDepend.Console.MultiOS.dll --cp ./DemoProject.ndproj ~/path/to/my/Microservice.sln

This created a default NDepend project for analysis of my microservice solution. This is a pretty big file (513 lines of XML) so I won’t paste it here.

As noted in the online docs, right now if you want to modify this project, you can do so by hand; you can work with the NDepend API; or you can use the currently-Windows-only GUI. I’m not going to modify the project because I’m curious what I will get with the default. Obviously this won’t include various custom queries and metrics I may normally run for my specific projects, but that’s OK for this.

Run the Analysis

Let’s see this thing go!

dotnet ~/ndepend/net8.0/NDepend.Console.MultiOS.dll ./DemoProject.ndproj

This kicks off the analysis (like you might see on a build server) and generates a nice HTML report.

NDepend report main page

I didn’t include coverage data in this particular run because I wanted to focus mostly on the code analysis side of things.

Since my service code broke some rules, the command line exited non-zero. This is great for build integration where I want to fail if rules get violated.

Dig Deeper

From that main report page, it looks like the code in the service I analyzed failed some of the default quality gates. Let’s go to the Quality Gates tab to see what happened.

NDepend report main page

Yow! Four critical rules violated. I’ll click on that to see what they were.

Broken critical rules

Looks like there’s a type that’s too big, some mutually dependent namespaces, a non-readonly static field, and a couple of types that have the same name. Some of this is easy enough to fix; some of it might require some tweaks to the rules, since the microservice has some data transfer objects and API models that look different but have the same data (so the same name in different namespaces is appropriate).

All in all, not bad!

I Still Love It

NDepend is still a great tool, even on Mac, and I still totally recommend it. To get the most out of it right now, you really need to be on Windows so you can get the GUI support, but for folks like me that are primarily Mac right now, it still provides some great value. Honestly, if you haven’t tried it yet, just go do that.

Room for Improvement

I always like providing some ideas on ways to make a good product even better, and this is no exception. I love this thing, and I want to see some cool improvements to make it “more awesomer.”

Installation Mechanism

I’m very used to installing things through Homebrew on Mac, and on Windows as we look at things like Chocolatey, WinGet, and others - it seems like having an installation that would enable me to use these mechanisms instead of going to a download screen on a website would be a big help. I would love to be able to do brew install ndepend and have that just work.

Visual Studio Code Support and Cross-Platform UI

There’s some integration in Visual Studio for setting up projects and running NDepend on the current project. It’d be awesome to see similar integration for VS Code.

At the time of this writing, the NDepend getting started on Mac documentation says that this is coming. I’m looking forward to it.

I’m hoping that whatever comes out, the great GUI experience to view the various graphs and charts will also be coming for cross-platform. That’s a huge job, but it would be awesome, especially since I’m not really on any Windows machines anymore.

Invoking the Command Line

The cross-platform executable is a .dll so running it is a somewhat long command line:

dotnet ~/path/to/net8.0/NDepend.Console.MultiOS.dll

It’d be nice if this was more… single-command, like ndepend-console or something. Perhaps if it was a dotnet global tool it would be easier. That would also take care of the install mechanism - dotnet tool install ndepend-console -g seems like it’d be pretty nifty.

Commands and Sub-Commands

The command line executable gets used to create projects, register licenses, and run analyses. I admit I’ve gotten used to commands taking command/sub-command hierarchies to help disambiguate the calls I’m making rather than having to mix-and-match command line arguments at the top. I think that’d be a nice improvement here.

For example, instead of…

dotnet ./net8.0/NDepend.Console.MultiOS.dll /reglic XXXXXXXX
dotnet ./net8.0/NDepend.Console.MultiOS.dll /unreglic

It could be…

dotnet ./net8.0/NDepend.Console.MultiOS.dll license register --code XXXXXXXX
dotnet ./net8.0/NDepend.Console.MultiOS.dll license unregister

That would mean when I need to create a project, maybe it’s under…

dotnet ./net8.0/NDepend.Console.MultiOS.dll project create [args]

And executing analysis might be under…

dotnet ./net8.0/NDepend.Console.MultiOS.dll analyze [args]

It’d make getting help a little easier, too, since the help could list the commands and sub-commands, with details being down at the sub-command level instead of right at the top.

Graphs in HTML Reports

Without the full GUI you don’t get to see the graphs like the dependency matrix that I love so much. Granted, these are far more useful if you can click on them and interact with them, but still, I miss them in the HTML.

Support for Roslyn Analyzers

NDepend came out long before Roslyn analyzers were a thing, and some of what makes NDepend shine are the great rules based on CQLinq - a much easier way to query for things in your codebase than trying to write a Roslyn analyzer.

It would be so sweet if the rules that could be analyzed at develop/build time - when we see Roslyn analyzer output - could actually be executed as part of the build. Perhaps it’d require pointing at an .ndproj file to get the list of rules. Perhaps not all rules would be something that can be analyzed that early in the build. I’m just thinking about the ability to “shift left” a bit and catch the failing quality gates before running the analysis. That could potentially lead to a new/different licensing model where some developers, who are not authorized to run “full NDepend,” might have cheaper licenses that allow running of CQL-as-Roslyn-analyzer for build purposes.

Maybe an alternative to that would be to have a code generator that “creates a Roslyn analyzer package” based on CQL rules. Someone licensed for full NDepend could build that package and other devs could reference it.

I’m not sure exactly how it would work, I’m kinda brainstorming. But the “shift left” concept along with catching things early does appeal to me.

halloween, maker, costumes comments edit

Back in 2021 we didn’t get to go to the usual Halloween party we attend due to COVID. That meant I didn’t really get to wear my Loki time prisoner costume. I decided I’d bring that one back out for this year so I could actually wear it.

Me as Prisoner Loki

This year we got to go to the party (no symptoms, even tested negative for COVID before walking out the door!) but ended up getting COVID for Halloween. That meant we didn’t hand out candy again, making this the second year in a row.

We did try to put a bowl of candy out with a “take one” sign. That didn’t last very long. While adults with small children were very good about taking one piece of candy per person, tweens and teens got really greedy really fast. We kind of expected that, but I’m always disappointed that people can’t just do the right thing; it’s always a selfish desire for more for me with no regard for you. Maybe that speaks to larger issues in society today? I dunno.

I need to start gathering ideas for next year’s costume. Since I reused a costume this year I didn’t really gather a lot of ideas or make anything, and I definitely missed that. On the other hand, my motivation lately has been a little low so it was also nice to not have to do anything.

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/

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.

Issue: Path and Environment Variable Propagation

Since you’ve only updated your path and environment from your shell profile (e.g., not /etc/paths or whatever), these changes won’t be available unless you’re running things from your login shell.

A great example is VS Code and build tools. Let’s say you have a build set up where the command is npm. If the path to npm is something you added in your ~/.profile, VS Code may not be able to find it.

  • If you start VS Code by running code from your shell, it will inherit the environment and npm will be found.
  • If you start VS Code by clicking on the icon in the Dock or Finder, it will not inherit the environment and npm will not be found.

You can mitigate a little of this, at least in VS Code, by:

  • Set your terminal.integrated.profiles.osx profiles to pass -l as an argument (act as a login shell, process ~/.profile) as shown in this Stack Overflow answer.
  • Set your terminal.integrated.automationProfile.osx profile to also pass -l as an argument to your shell. (You may or may not need to do this; I was able to get away without it.)
  • Always use shell commands to launch builds (specify "type": "shell" in tasks.json) for things instead of letting it default to "type": "process".

Other tools will, of course, require other workarounds.

Issue: Python Config During Updates

Some packages like glib have a dependency on Python for installs. However, if you have configuration settings you may need to set (for example, trusted-host), with Python being in your user folder, you may not have the rights to write to /Library/Application Support/pip to set a global config. However, sometimes these installers ignore user-level config. In this case, you may need to put your pip.conf in the folder with Python, for example ~/local/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/pip.conf.


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!