CLAUDE LABJP
OUTPUT — v2.1.237 ships a built-in Concise output style that leads with results and skips preamble, without cutting any corners on the work itself. Pick it under Output style in /configCACHE — v2.1.237 fixes prompt caching for sessions running through an LLM gateway or a custom base URL. If you work behind a company proxy, your effective per-token cost just changedCONFIG — v2.1.236 adds ANTHROPIC_DEFAULT_MODEL to set which model new sessions start on. A /model pick still overrides it and persists across restarts, unlike ANTHROPIC_MODELNOTIFY — With notify_when_idle in v2.1.236, one Claude Code session can ask another on the same machine for a single heads-up when it next goes idle. Opt-in, one-shot, no pollingSECURITY — On macOS, wildcard read-deny rules now win inside allowed read regions, cover the contents of matched directories, and can no longer be sidestepped by renaming the filePRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31, with standard $3 and $15 pricing starting September 1. Ten days to goOUTPUT — v2.1.237 ships a built-in Concise output style that leads with results and skips preamble, without cutting any corners on the work itself. Pick it under Output style in /configCACHE — v2.1.237 fixes prompt caching for sessions running through an LLM gateway or a custom base URL. If you work behind a company proxy, your effective per-token cost just changedCONFIG — v2.1.236 adds ANTHROPIC_DEFAULT_MODEL to set which model new sessions start on. A /model pick still overrides it and persists across restarts, unlike ANTHROPIC_MODELNOTIFY — With notify_when_idle in v2.1.236, one Claude Code session can ask another on the same machine for a single heads-up when it next goes idle. Opt-in, one-shot, no pollingSECURITY — On macOS, wildcard read-deny rules now win inside allowed read regions, cover the contents of matched directories, and can no longer be sidestepped by renaming the filePRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31, with standard $3 and $15 pricing starting September 1. Ten days to go
Articles/Claude Code
Claude Code/2026-08-21Intermediate

Adding ANTHROPIC_DEFAULT_MODEL Changes Nothing While ANTHROPIC_MODEL Is Still Set

ANTHROPIC_DEFAULT_MODEL, added in v2.1.236, is not a replacement for ANTHROPIC_MODEL. Here is how the two differ, plus a Python script that lists every place a model can be pinned.

Claude Code229environment variables2model selection3configuration8

I open the same repository from an editor terminal during the day and from a separate shell at night. One evening I noticed that only the night sessions were starting on a model name that was several releases old.

The cause turned out to be me, a few months earlier. I had wanted to pin a model temporarily and wrote export ANTHROPIC_MODEL=... into ~/.zshrc. That line never came back out. Anything written into a shell profile never shows up in the interface — it is silently injected every time you open a terminal, so the only clue is a vague sense that the responses feel different.

When ANTHROPIC_DEFAULT_MODEL arrived in v2.1.236, my first thought was that switching to it would fix the problem. It did not. The two variables play different roles, and while one of them is set, the other never gets a chance to appear.

The two variables do not replace each other

ANTHROPIC_MODEL has been around for a while. While it is set, it pins the session to that model. ANTHROPIC_DEFAULT_MODEL is the new one, and it decides which model a new session starts on. If you pick a different model with /model during the session, your choice wins and it persists across restarts.

So the first is a fixed assignment and the second is a starting value you are meant to move. When both are present, the fixed one wins. Add the new variable without removing the old one and not a single session changes where it starts.

Where the model is setScopeCan /model override it mid-session?How easy is it to forget?
ANTHROPIC_MODELEvery session while it is setIt is a deliberate pin, so normally you leave it aloneVery easy — invisible when it lives in a shell profile
ANTHROPIC_DEFAULT_MODELThe starting model of new sessionsYes, and the choice survives restartsModerate
model in settings.jsonThat user or that projectYesLow — it is a visible file
A model set inside the env block of settings.jsonEvery session in that projectBehaves like a pinVery easy — invisible if you only read the model field

The last two columns are where the trouble lives. Four or more places hold what looks like the same string — a model name — but they behave differently, and two of them sit somewhere you are unlikely to look. It took me several round trips between /status and my config files before I understood what was actually happening.

List everything that could be deciding your model

What I needed was not a prediction of which setting wins. It was a single screen showing what is set at all. This small script walks the environment variables, user settings, project settings, local settings, and shell profiles, and separates pins from starting values.

#!/usr/bin/env python3
"""List every setting that could decide the Claude Code execution model.
 
Usage:
    python3 model_sources.py [project root]
 
With no argument, the current directory is treated as the project root.
"""
import json
import os
import re
import sys
import unicodedata
 
# Allow the home directory to be swapped out so the script is testable
HOME = os.environ.get("MODEL_SOURCES_HOME") or os.path.expanduser("~")
 
# These variables behave differently. Confusing them is how you misread restarts.
ENV_VARS = [
    ("ANTHROPIC_MODEL", "pin", "pins every session while it is set"),
    ("ANTHROPIC_DEFAULT_MODEL", "default", "starting model for new sessions; /model wins"),
    ("ANTHROPIC_SMALL_FAST_MODEL", "aux", "helper work only; does not change the main model"),
]
 
# Later entries are closer to the work (project beats user)
SETTINGS_FILES = [
    ("user settings", os.path.join(HOME, ".claude", "settings.json")),
    ("project settings", os.path.join("{root}", ".claude", "settings.json")),
    ("local settings", os.path.join("{root}", ".claude", "settings.local.json")),
]
 
# The usual places a forgotten export hides
PROFILES = [".zshrc", ".zshenv", ".bashrc", ".bash_profile", ".profile"]
 
JSONC_COMMENT = re.compile(r"^\s*//")
 
 
def load_jsonc(path):
    """Read JSON while tolerating // line comments. Never raise on bad input."""
    try:
        with open(path, encoding="utf-8") as fh:
            body = "".join(l for l in fh if not JSONC_COMMENT.match(l))
        return json.loads(body), None
    except FileNotFoundError:
        return None, None
    except (json.JSONDecodeError, OSError) as exc:
        return None, f"could not read: {exc}"
 
 
def collect(root):
    found = []
 
    for name, kind, note in ENV_VARS:
        value = os.environ.get(name)
        if value:
            found.append(("environment", name, value, kind, note))
 
    for label, template in SETTINGS_FILES:
        path = template.format(root=root)
        data, error = load_jsonc(path)
        if error:
            # Swallowing a broken config is how "I set it and nothing happened" starts
            found.append((label, path, "-", "error", error))
            continue
        if not data:
            continue
        if data.get("model"):
            found.append((label, path, data["model"], "config", "model field"))
        nested = (data.get("env") or {}).get("ANTHROPIC_MODEL")
        if nested:
            found.append((label, path, nested, "pin", "env block injects ANTHROPIC_MODEL"))
 
    for name in PROFILES:
        path = os.path.join(HOME, name)
        try:
            with open(path, encoding="utf-8") as fh:
                lines = fh.readlines()
        except OSError:
            continue
        for lineno, line in enumerate(lines, 1):
            if "ANTHROPIC_MODEL" in line or "ANTHROPIC_DEFAULT_MODEL" in line:
                found.append((
                    "shell profile", f"{path}:{lineno}", line.strip(), "shell",
                    "re-injected every time you open a shell",
                ))
    return found
 
 
def display_width(text):
    """Count wide characters as two columns so the table stays aligned."""
    return sum(2 if unicodedata.east_asian_width(ch) in "WF" else 1 for ch in text)
 
 
def pad(text, width):
    return text + " " * max(0, width - display_width(text))
 
 
def main():
    root = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    rows = collect(root)
 
    if not rows:
        print("Nothing is setting a model (the CLI default applies)")
        return 0
 
    width = max(display_width(r[0]) for r in rows)
    print(f"{pad('source', width)}  kind      value / location")
    print("-" * 72)
    for origin, where, value, kind, note in rows:
        print(f"{pad(origin, width)}  {kind.ljust(8)}  {value}")
        print(f"{' ' * width}{where}{note}")
 
    pins = {r[2] for r in rows if r[3] == "pin"}
    defaults = {r[2] for r in rows if r[3] in ("default", "config")}
 
    print()
    if len(pins) > 1:
        print(f"WARNING: {len(pins)} conflicting pins: {', '.join(sorted(pins))}")
    if pins and defaults - pins:
        print(f"WARNING: pins {sorted(pins)} make defaults {sorted(defaults - pins)} unreachable")
    if not pins and len(defaults) > 1:
        print(f"NOTE: {len(defaults)} candidate defaults (the closest one wins)")
    return 1 if len(pins) > 1 else 0
 
 
if __name__ == "__main__":
    sys.exit(main())

Three choices in there are deliberate.

It reads inside the env block of settings.json, not just the model field. Checking only model and concluding "project settings are empty, so they cannot be the cause" is exactly how the hardest pin to find stays hidden.

It reports broken JSON as a row instead of swallowing the error. When a config file fails to parse, the symptom you experience is "I set it and nothing happened" — cause and symptom are far apart, so this one should never be silent.

It exits with status 1 when more than one pin is present. You do not need that if you are reading the output yourself, but setups like this tend to break right after you stop reading them.

What it printed on my machine

Here is the output against a configuration set that mirrors mine — user settings, project settings, local settings, a shell profile, and live environment variables.

source            kind      value / location
------------------------------------------------------------------------
environment       pin       claude-opus-4-1-20250805
                            └ ANTHROPIC_MODEL — pins every session while it is set
environment       default   claude-sonnet-5
                            └ ANTHROPIC_DEFAULT_MODEL — starting model for new sessions; /model wins
user settings     config    claude-sonnet-5
                            └ /home/me/.claude/settings.json — model field
project settings  config    claude-opus-5
                            └ /work/proj/.claude/settings.json — model field
project settings  pin       claude-opus-5
                            └ /work/proj/.claude/settings.json — env block injects ANTHROPIC_MODEL
local settings    config    claude-haiku-4-5-20251001
                            └ /work/proj/.claude/settings.local.json — model field
shell profile     shell     export ANTHROPIC_MODEL="claude-opus-4-1-20250805"
                            └ /home/me/.zshrc:2 — re-injected every time you open a shell
shell profile     shell     export ANTHROPIC_DEFAULT_MODEL="claude-sonnet-5"
                            └ /home/me/.bash_profile:1 — re-injected every time you open a shell
 
WARNING: 2 conflicting pins: claude-opus-4-1-20250805, claude-opus-5
WARNING: pins ['claude-opus-4-1-20250805', 'claude-opus-5'] make defaults ['claude-haiku-4-5-20251001', 'claude-sonnet-5'] unreachable

Read it top down. Of those eight rows, only the first one decides which model a session actually starts on. Four of the remaining rows are settings someone wrote believing they were in effect. ANTHROPIC_DEFAULT_MODEL, the user-level model, and the local model are never consulted in this state.

Remove ANTHROPIC_MODEL from the shell profile and drop the env block from the project settings, and the same script prints this instead.

source            kind      value / location
------------------------------------------------------------------------
environment       default   claude-sonnet-5
                            └ ANTHROPIC_DEFAULT_MODEL — starting model for new sessions; /model wins
user settings     config    claude-sonnet-5
                            └ /home/me/.claude/settings.json — model field
project settings  config    claude-opus-5
                            └ /work/proj/.claude/settings.json — model field
local settings    config    claude-haiku-4-5-20251001
                            └ /work/proj/.claude/settings.local.json — model field
shell profile     shell     export ANTHROPIC_DEFAULT_MODEL="claude-sonnet-5"
                            └ /home/me/.bash_profile:1 — re-injected every time you open a shell
 
NOTE: 3 candidate defaults (the closest one wins)

The warning changed from a conflict into a note about multiple candidates. In this state, whichever one is chosen can still be overridden with /model. Once the pins are gone, the configuration becomes something a human can reason about again.

One limitation worth naming: this script tells you what is set, not what ultimately wins. For that, start claude and read /status. Precedence details can shift between versions, so keeping the inventory and the measurement separate is the safer habit.

Where I ended up putting things

As an indie developer maintaining iOS and Android apps side by side, I want different models in different repositories. A project where the work is mostly reading existing code and a project where I am building a screen from scratch are not well served by the same choice. Even so, the number of places holding that choice needed to come down.

This is where it settled.

  1. No model settings in shell profiles at all. Removing everything that takes effect where I cannot see it was the highest priority
  2. One everyday model, expressed through ANTHROPIC_DEFAULT_MODEL. It is a starting point, and /model moves it when the day's work calls for something else
  3. Per-project differences go in model inside .claude/settings.json. It lives in a file, so it is still visible to me six months from now
  4. When I really do need a pin, it goes on the command itself. ANTHROPIC_MODEL=claude-haiku-4-5-20251001 claude -p "..." keeps the lifetime of the pin to a single invocation

The fourth one made the biggest difference. Pins are a legitimate tool, but the moment you extend their lifetime they turn into settings people forget. Attaching one to the command instead of a profile removed almost all of the later detective work.

For how to choose between models in the first place, Claude Code Model Selection Strategy covers the reasoning by use case. If you need a record of which model an unattended job actually ran on, Which Model Ran Last Night's Unattended Session? goes into that.

Three things to verify after you change anything

Run these three checks right after you touch the configuration and you will save yourself the confusion later.

Open a new shell and run env | grep ANTHROPIC. The shell you were editing in still holds the old values, so reopening matters. Whatever appears here is what your next session inherits.

Start claude and read the model shown by /status. If the inventory and the live session agree, your configuration is doing what you think it is doing.

Then put the script somewhere its exit code is used. I run it as a preflight step before unattended jobs and stop the job when two or more pins turn up. If you want pinning handled as a system rather than a habit, including credit ceilings, Pin Your Execution Model with enforceAvailableModels describes a more thorough design.

If you only do one thing today, run grep -rn ANTHROPIC_MODEL ~/.zshrc ~/.bashrc ~/.profile. Nothing returned means no invisible pin exists. Anything returned is what is deciding your model right now.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-07-19
I Could No Longer Remember What I'd Changed in Auto Mode — Where claude auto-mode reset Fits In
Running nightly automation for weeks, I kept nudging my auto-mode settings one at a time. One morning a small oddity made me realize I could not recall what I had changed. Here is how I rebuilt my configuration around claude auto-mode reset as a known-good baseline, from a solo developer's field notes.
Claude Code2026-05-11
Claude Code MCP Server Won't Start — How to Fix "spawn npx ENOENT" and PATH Issues
You configured an MCP server in Claude Code's settings.json, but it never starts — just "spawn npx ENOENT" or "spawn uvx ENOENT" errors. The culprit is a PATH mismatch between your shell and Claude Code's spawning environment. Here's how to diagnose and fix it.
Claude Code2026-04-24
Claude Code Environment Variables — The Complete Practical Reference
A practitioner-focused reference for every Claude Code environment variable that actually matters. Covers API connectivity, model selection, PowerShell tool quirks, logging, timeouts, security flags, and real-world templates for solo, team, and CI/CD setups.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →