Pinning Claude Code
Earlier today I saw this post from Curtis Poe.
Just yesterday I pinned my Claude Code to Opus 4.8 for similar reasons. Opus 5 doesn’t seem to be a clear improvement over 4.8 and, in some ways, it just feels worse. I can’t provide you with benchmarks to prove it, so this is mostly hand waving, but I feel like the results I’m getting are worse, more verbose, wider in scope and sometimes just mystifying. It’s taking longer to do basic things. I’m happy to switch back to 4.8 for as long as it’s available. Newer does not automatically mean better. If you’ve ever released your own software, you likely know this to be true.
Curtis’ post contains a quick workaround for this, but I do something different, so I figured I’d show that off. And when I say that I do something different, it’s just a script that Claude wrote for me. (Claude, however, did not write this post.)
A few weeks ago, I wrote about being bitten by a Claude upgrade Easter Egg: Claude Code: Anatomy of a Misfeature. I was pleased to later read an article by Mauricio Patiño Cervantes which referenced my piece. I was also happy to see that this article was advocating for a way of managing Claude’s defaults that is similar to mine.
#On the Joys of Pinning
Photo: Safety Pin by Haragayato, licensed under CC BY-SA 3.0.
I’ve come to the conclusion that I prefer boring upgrades when it comes to Claude Code, so I’ve been pumping the brakes on the YOLO mode that it defaults to. There is more than one way to do this, but I wanted something that would instantly stick across all of my new sessions without requiring me to do much, since I’m against busy work. It turns out you can force environment variables via Claude’s config. That allows me to have env vars propagate instantly to new Claude sessions without personally having to update the env in all of my far too many tmux sessions.
So, I end up with a config that looks something like this (truncated) version:
{
"env": {
"DISABLE_AUTOUPDATER": "1",
"FORCE_AUTOUPDATE_PLUGINS": "1"
},
"model": "claude-opus-4-8",
"availableModels": [
"claude-opus-4-8",
"opus",
"sonnet",
"haiku"
]
}
The highlights here are: DISABLE_AUTOUPDATER stops Claude from happily updating itself in the background, but it also stops plugins from doing the same. I have a higher risk tolerance for plugins, so I re-enable that behaviour via FORCE_AUTOUPDATE_PLUGINS. I then choose a default model via model and create a list of models I’d generally like to choose from. You’ll note that Fable is missing from that list because the last time I ran /model fable the only thing I saw was a lot of credits go up in smoke. I think what I do is just not complex enough to need Fable and I’m not super jazzed about things that can get turned on and off on the whims of a foreign government.
So that gives me the pin that goes back to Opus 4.8, but I’m not doing this by hand. I have too many places where my dot-files live, so I want this to be scriptable and idempotent. I give you my config and (caveat emptor) the LLM-generated portion of tonight’s entertainment. It currently lives in my dot-files: configure/claude-settings.sh.
Contains LLM-generated content not written by Olaf
#!/usr/bin/env bash
# Force host-level Claude Code settings that must not live in this repo.
# ~/.claude/settings.json is deliberately untracked (it holds the plugin list
# and marketplace config), so we merge individual keys into whatever is
# already there rather than symlinking or overwriting the file.
#
# bin/nn sets the same env vars for nono-sandboxed sessions. Setting them here
# covers plain `claude` runs, which never go through bin/nn.
set -eu -o pipefail
SETTINGS="$HOME/.claude/settings.json"
mkdir -p "$(dirname "$SETTINGS")"
if [[ ! -f $SETTINGS ]]; then
echo '{}' >"$SETTINGS"
fi
set_env_key() {
local key=$1 value=$2
if jq -e --arg k "$key" --arg v "$value" '.env[$k] == $v' "$SETTINGS" >/dev/null; then
return
fi
jq --arg k "$key" --arg v "$value" '.env[$k] = $v' "$SETTINGS" >"$SETTINGS.tmp"
mv "$SETTINGS.tmp" "$SETTINGS"
echo "set env.$key=$value in $SETTINGS"
}
# Like set_env_key, but for a top-level key whose value is raw JSON (bool,
# number, string, object) rather than a string env var.
set_key() {
local key=$1 value=$2
if jq -e --arg k "$key" --argjson v "$value" '.[$k] == $v' "$SETTINGS" >/dev/null; then
return
fi
jq --arg k "$key" --argjson v "$value" '.[$k] = $v' "$SETTINGS" >"$SETTINGS.tmp"
mv "$SETTINGS.tmp" "$SETTINGS"
echo "set $key=$value in $SETTINGS"
}
# Pin the claude binary; installer/claude.sh owns which version we land on.
set_env_key DISABLE_AUTOUPDATER 1
# ...but keep plugins updating, which DISABLE_AUTOUPDATER would otherwise
# freeze alongside the CLI.
set_env_key FORCE_AUTOUPDATE_PLUGINS 1
# Compact automatically instead of needing a manual /compact. Compaction fires
# at window-33k (20k reserved for output, 13k buffer), and the window itself is
# clamped to the model max -- so on a 200k model anything above 200000 is a
# no-op, and anything below just compacts sooner for no saving. 200000 is the
# max useful value; it also keeps the clamp meaningful if 1M context is ever
# re-enabled above.
set_key autoCompactEnabled true
set_key autoCompactWindow 200000
# Stay on 200k-class context instead of the native 1M window some models (e.g.
# Sonnet 5) offer. Long context is rarely worth the spend here, and a hard
# ceiling keeps sessions focused. This is the setting that actually caps token
# spend -- autoCompactWindow above only moves when compaction fires.
set_env_key CLAUDE_CODE_DISABLE_1M_CONTEXT 1
# Cap concurrently-running subagents so one message can't fan out unbounded
# background agents. Added in claude 2.1.217, where it defaults to 20; inert on
# older versions. DISABLE_AUTOUPDATER above means installer/claude.sh decides
# when we actually land on a version that reads this.
set_env_key CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS 5
# Allowlist the models offered by /model, --model, ANTHROPIC_MODEL and resume,
# which keeps Fable out of the picker. Experimental: unverified whether this key
# is honored; harmless if ignored since it only narrows an allowlist. The pinned
# opus id below is listed explicitly so it matches the allowlist exactly rather
# than relying on the "opus" family alias, which resolves to the newest Opus.
set_key availableModels '["claude-opus-4-8","opus","sonnet","haiku"]'
# Default new sessions to Opus 4.8 rather than whatever the "opus" alias points
# at today (Opus 5). /model overwrites this in ~/.claude/settings.json for the
# session; --model and ANTHROPIC_MODEL override for a single run.
set_key model '"claude-opus-4-8"'
# Default new sessions to medium reasoning effort to trim thinking-token spend.
# /effort overwrites this in ~/.claude/settings.json; --effort and
# CLAUDE_CODE_EFFORT_LEVEL override for a single session. Accepts
# low/medium/high/xhigh (no "max").
set_key effortLevel '"medium"'
So far this has worked quite well for me. I can now upgrade Claude Code on my own schedule and tweak config settings in various environments without having to do anything other than run my dot-files installer.

