The problem Link to heading

Valheim ships a universal macOS build, so it runs natively on Apple Silicon. BepInEx 5 ships a universal macOS build too. Put them together and you get a game that launches, plays fine, and loads exactly zero plugins.

There is no error. There is no BepInEx/LogOutput.log. There is nothing in Player.log and nothing on stdout. The game behaves as though BepInEx were never installed, which is what makes this expensive to debug — every obvious suspect looks innocent because nothing ever complains.

I spent an afternoon on this. The fix is one string in one file.

What I tested against Link to heading

Component Version
Machine Apple M4 Pro, macOS 26 (Darwin 25.6.0)
Valheim build 25390630, Unity 6000.0.75f1, Mono
Valheim.app universal, x86_64 arm64
BepInEx 5.4.23.5, BepInEx_macos_universal_5.4.23.5.zip
UnityDoorstop 4.5.0

The game directory is:

~/Library/Application\ Support/Steam/steamapps/common/Valheim

Install BepInEx Link to heading

Download BepInEx_macos_universal_5.4.23.5.zip from the BepInEx releases page and unzip it directly into the game directory, next to Valheim.app. You should end up with:

Valheim/
├── Valheim.app
├── BepInEx/
├── libdoorstop.dylib
├── run_bepinex.sh
└── .doorstop_version

macOS will quarantine the downloaded files. Clear that before you go any further, or the dylib will be blocked without telling you why:

cd ~/Library/Application\ Support/Steam/steamapps/common/Valheim
xattr -dr com.apple.quarantine libdoorstop.dylib BepInEx
chmod +x run_bepinex.sh

Run the game once through the script to confirm BepInEx at least writes its config:

./run_bepinex.sh ./Valheim.app

After that run you will have BepInEx/config/BepInEx.cfg. You will not have BepInEx/LogOutput.log. That gap is the bug.

The one line that matters Link to heading

Open run_bepinex.sh and find this block near the bottom — line 324 in 5.4.23.5:

if [ -n "${is_apple_silicon}" ]; then
    export ARCHPREFERENCE="arm64,x86_64"
    ...
    exec arch -e DYLD_INSERT_LIBRARIES="${DYLD_INSERT_LIBRARIES}" "$executable_path" "$@"

Reverse the two architectures:

    export ARCHPREFERENCE="x86_64,arm64"

Launch again and LogOutput.log appears within ten seconds, with your plugins in it.

That preference order is the only lever. Wrapping the script in arch -x86_64 does nothing, because the script re-exports ARCHPREFERENCE and re-execs through arch itself.

Why it fails Link to heading

BepInEx’s preloader runs HarmonyInteropFix.Apply() as one of its first acts, which is the first Harmony patch it ever applies, and which needs MonoMod to write to executable memory. On Apple Silicon that requires pthread_jit_write_protect_np() and the com.apple.security.cs.allow-jit entitlement rather than the mprotect MonoMod reaches for. Under Rosetta the restriction relaxes, and the same code path succeeds.

I have evidence for where it throws and a hypothesis for why. BepInEx.cfg contains the keys bound before that call and none of the keys bound after it, which brackets the failure tightly.

The silence is a second, separate bug. Preloader.Run() wraps its body in a catch that calls Logger.LogFatal, but the first log listener is not registered until after the throwing line. The fatal message goes to a logger with no listeners and is discarded. A Console.Error.WriteLine fallback would have turned that afternoon into one line of output.

Worth saying plainly: ARCHPREFERENCE="arm64,x86_64" is deliberate upstream, and the comment next to it explains the intent — stop an x86_64 parent process from silently downgrading a universal game binary. That reasoning is sound. It just does not account for BepInEx 5 being unable to finish its own preloader on arm64.

Surviving Steam updates Link to heading

Every Steam update restores the stock run_bepinex.sh, and your mods go quiet again without saying anything. Rather than remember the edit, wrap it. I keep this as launch-modded.sh in the game directory:

#!/bin/sh
V="$HOME/Library/Application Support/Steam/steamapps/common/Valheim"
cd "$V" || exit 1

# A Steam update restores the stock run_bepinex.sh, and native arm64 then loads
# no plugins while saying nothing at all. Re-apply the one edit that matters.
if ! grep -q 'ARCHPREFERENCE="x86_64,arm64"' run_bepinex.sh; then
    sed -i '' 's/export ARCHPREFERENCE=.*/export ARCHPREFERENCE="x86_64,arm64"/' run_bepinex.sh
fi

if [ $# -gt 0 ]; then
    exec ./run_bepinex.sh "$@"
fi
exec ./run_bepinex.sh ./Valheim.app
chmod +x launch-modded.sh

Configure Steam Link to heading

Steam’s Play button starts Valheim.app directly and loads no plugins at all. Point it at the wrapper instead.

Right-click Valheim in your library, then Properties → General → Launch Options, and enter the script path followed by %command%:

"/Users/you/Library/Application Support/Steam/steamapps/common/Valheim/launch-modded.sh" %command%

The quotes are required — the path contains a space. Steam passes the inner executable path as the argument, and run_bepinex.sh accepts either that or the .app, so launching from Steam and launching from a terminal both work.

Launching through Steam rather than by hand is what keeps the Steam overlay, playtime tracking and friend invites working.

Installing mods Link to heading

BepInEx loads any DLL it finds in BepInEx/plugins. There is no macOS mod manager worth the trouble here, so install by hand:

  1. Download the mod from Thunderstore.
  2. Unzip it. The DLL is usually at the top level or under a plugins/ directory.
  3. Copy the DLL into BepInEx/plugins/.
  4. Clear the quarantine flag: xattr -dr com.apple.quarantine BepInEx/plugins.

Some mods ship a folder of assets alongside the DLL. Copy the whole folder, keeping its structure.

Mods generate their config on first launch, not at install time. Start the game once, quit, then edit the .cfg file that appears in BepInEx/config/. My install looks like this:

BepInEx/
├── plugins/
│   ├── Dobbo.Tracker.Client.dll
│   └── Dobbo.Tracker.Shared.dll
├── config/
│   ├── BepInEx.cfg
│   └── ca.dobbo.tracker.client.cfg
└── LogOutput.log

If you play on a server, the mods that change game behaviour generally have to be installed on both sides at matching versions. Client-only mods — UI, map, quality of life — do not.

Checking it worked Link to heading

head -5 "$HOME/Library/Application Support/Steam/steamapps/common/Valheim/BepInEx/LogOutput.log"
[Message:   BepInEx] BepInEx 5.4.23.5 - Valheim (2026-09-18 7:24:32 AM)
[Info   :   BepInEx] Detected Unity version: v6000.0.75f1
[Message:   BepInEx] Preloader started
[Info   :   BepInEx] Patching [UnityEngine.CoreModule] with [BepInEx.Chainloader]
[Message:   BepInEx] Preloader finished

Then look for a Loading [<your mod>] line further down. If the file does not exist at all, the ARCHPREFERENCE edit did not take — check that Steam is launching the wrapper and not the app.

You can confirm the architecture directly. With the game running, Activity Monitor shows the Valheim process as Intel rather than Apple. From a terminal, vmmap on the pid reports Code Type: X86-64 (translated) when it is working, and Code Type: ARM64 when it is not.

The cost Link to heading

This runs the game translated under Rosetta, and you pay for it in frame rate. On an M4 Pro I find it perfectly playable, but it is not free, and it is not something I would ask a group of friends to do to join a server.

Native arm64 BepInEx is the real fix and it belongs upstream, in MonoMod’s detour platform selection for Darwin arm64. BepInEx 6 carries a much newer MonoMod and may already handle it; I have not tested that yet.

Feedback Link to heading

The root cause here is narrowed rather than proven — I have bracketed the throw to HarmonyInteropFix.Apply() and I have a plausible mechanism, but I have not yet captured the exception itself. If you have, or if BepInEx 6 loads natively on your machine, I would like to hear about it.