PhilipMat

Getting `fnm` to clean up its auto-links

fnm is a fantastically fast node manager, which make it a pleasure to use over nvm.

fnm handles its own node versions and it observes .node-version files in folder, automatically installing/loading the proper node version.

Part of how it does that:

  1. It keeps a user-global installation in $HOME/Library/Application\ Support/fnm, and it’s where it stores the various node versions installed by the user.
  2. It creates links to those version in $HOME/.local/state/fnm_multishells, one link per shell.

The side-effect of (2) is that every time you open a shell, a new link is created. And fnm does not clean up when the shell exits. This is by design – so that different shells can have different node versions.
No big deal, but these links can accumulate over time. When I cleaned up my fnm_multishells I had some 3k links in there.

You can clean these up periodically, or you can set a trap EXIT function in zsh and bash to clean it up. Luckily, fnm uses the $FNM_MULTISHELL_PATH to point to the exact sub-folder where the links are created.

Add this to your .zshrc to clean up after fnm.

# fnm creates a bunch of links into $FNM_MULTISHELL_PATH = ~/.local/state/fnm_multishells
# one link per shell instance and it never cleans them up
# this function attempts to do that on shell exit
cleanup_fnm() {
  if [[ -n "$FNM_MULTISHELL_PATH" && -d "$FNM_MULTISHELL_PATH" ]]; then
    rm -rf "$FNM_MULTISHELL_PATH"
  fi
}
trap cleanup_fnm EXIT

Copying ignored files to new git worktrees

When creating a git worktree, files ignored by .gitignore are (rightfully) not copied.

But often we exclude in .gitignore files that are useful for development. For example, .env files; or for .NET, appsettings.local.json files. If the intent of the worktree is to have a useful-for-development folder and branch, then these files need to be copied over.

Here’s my solution involving post-checkout hook (.git/hooks/post-checkout) and a .worktreeinclude file:

#!/usr/bin/env bash
# Checking out a new worktree (that’s what the "$1" == "0000..." is all about)?
if [[ "$1" == "0000000000000000000000000000000000000000" ]]; then
	MAIN_WORKTREE=$(git worktree list | head -1 | awk '{print $1}')
	COPY_LIST="$MAIN_WORKTREE/.worktreeinclude"

	[[ ! -f "$COPY_LIST" ]] && exit 0

	while IFS= read -r f || [[ -n "$f" ]]; do
  	  [[ -z "$f" || "$f" == \#* ]] && continue
  	  if [[ -e "$MAIN_WORKTREE/$f" && ! -e "$f" ]]; then
    	cp -r "$MAIN_WORKTREE/$f" "$f"
    	echo "Copied $f"
  	  fi
	done < "$COPY_LIST"
fi

chmod u+x .git/hooks/post-checkout

This script gets invoked on any git checkout operation, which git worktree add does.
It detects if it’s a new worktree and then it copies the files found in the .worktreeinclude to the new directory that git worktree add created.

Tip: fd "appsettings.*.local.json" -I |grep -viE "bin|obj" > .worktreeinclude to capture all useful appsettings*.local.json files in the repo.

Improvements

  1. Use git templates to have this file copied to all new repos. Also git init after template configuration re-initializes the repo.
    Create a ~/.git_template folder and add it to ~/.gitconfig`:
[init]
  templateDir = ~/.git_template

Then run git init in the repo of your choice.

  1. Add support for multiple hooks:
    1. mkdir -p ~/.git_template/hooks/post-checkout.d
    2. Copy the post-checkout script from above to ~/.git_template/hooks/post-checkout.d/worktree-include
    3. Create ~/.git_template/hooks/post-checkout with:
#!/usr/bin/env bash
#
# This script should be saved in a git repo as a hook file, e.g. .git/hooks/pre-receive.
# It looks for scripts in the .git/hooks/pre-receive.d directory and executes them in order,
# passing along stdin. If any script exits with a non-zero status, this script exits.

script_dir=$(dirname $0)
hook_name=$(basename $0)

hook_dir="$script_dir/$hook_name.d"

if [[ -d $hook_dir ]]; then
  stdin=$(cat /dev/stdin)

  for hook in $hook_dir/*; do
    echo "Running $hook_name/$hook hook"
    echo "$stdin" | $hook "$@"

    exit_code=$?

    if [ $exit_code != 0 ]; then
      exit $exit_code
    fi
  done
fi

exit 0
  1. chmod u+x ~/.git_template/hooks/post-checkout && chmod u+x ~/.git_template/hooks/post-checkout.d/*

Now you can run git init in any repo and it will create these scripts. I don’t know what it does if you already have scripts like those in the local repo.

Example of running this:

$ g worktree add -B 13695-common_user_site ../Service.Repo-13695 master
Preparing worktree (new branch '13695-common_user_site')
HEAD is now at XXXXXXXX Merge pull request #612 from XXXXXXXX
Running post-checkout//Users/philip/Projects/Service.Repo/.git/hooks/post-checkout.d/worktree-include hook
Copied Service.API/appsettings.local.json
Copied Service.Listener/appsettings.local.json
Copied Core.Service.Test.Integration/appsettings-test.local.json
Copied appsettings-cake.local.json

Thanks to the following posts:

Good designs converge

Wonderful essay by Paul Graham: The Brand Age. I learned a lot about watch history from it.

I like how he positions branding and design as opposite:

Branding isn’t merely orthogonal to good design, but opposed to it. Branding by definition has to be distinctive. But good design, like math or science, seeks the right answer, and right answers tend to converge. […] It’s the same if you want to set your designs apart. If you choose good options, other people will choose them too.

Which shortly leads to pointing out that religion also has its followers do inconvenient and unreasonable things:

Indeed, the conflict between branding and design is so fundamental that it extends far beyond things we call design. We see it even in religion. If you want the adherents of a religion to have customs that set them apart from everyone else, you can’t make them do things that are convenient or reasonable, or other people would do them too. If you want to set your adherents apart, you have to make them do things that are inconvenient and unreasonable.

Ties in well with:

Brand age watches look strange because they have no practical function. Their function is to express brand, and while that is certainly a constraint, it’s not the clean kind of constraint that generates good things.

TIL: Adding custom wallpaper folders to macOS

A nice, notarized program, that makes adding a wallpaper folder as easy as /usr/local/bin/wallpaper-folder add ~/Pictures/Wallpapers

AI Summary

The article explains how macOS wallpaper folder handling changed in macOS 26 and how that broke simple scripted approaches admins used to add brand or custom wallpaper collections. Previously you could drop images into /Library/Desktop Pictures (which placed them after built-ins) or add a user folder entry in ~/Library/Preferences/com.apple.systempreferences.plist (DSKDesktopPrefPane:UserFolderPaths) using tools like PlistBuddy; those techniques worked across macOS 13–15.

In macOS 26 the wallpaper configuration moved into a container at ~/Library/Containers/com.apple.wallpaper.extension.image/ with embedded plists and a WallpaperAgent daemon, making shell-based edits impractical. To address this, the author created WallpaperFolderManager, a Swift utility/package that generates the required files and encoded plists, restarts cfprefsd and WallpaperAgent, and exposes commands to add, list, and remove wallpaper folders. It’s available as a signed notarized pkg, standalone binary, or Swift package, has options for verbose output and skipping service restarts, and was tested on macOS 15 and 26 (with fallback behavior for 13–15).

Source: TIL: Adding custom wallpaper folders to macOS

TIL: GitHub 10x+ Commit Surge in 2026

Kyle Daigle, COO of Github, responding to an tweet with some data about the insane amount of code GH sees as the result of AI:

There were 1 billion commits in 2025. Now, it’s 275 million per week, on pace for 14 billion this year if growth remains linear (spoiler: it won’t.)

GitHub Actions has grown from 500M minutes/week in 2023 to 1B minutes/week in 2025, and now 2.1B minutes so far this week.