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:
- Download the mod from Thunderstore.
- Unzip it. The DLL is usually at the top level or under a
plugins/directory. - Copy the DLL into
BepInEx/plugins/. - 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.