TL;DR and verdict
When your Claude plan limit hits mid-task, you can run the same Claude Code against a model on your own laptop through Ollama. We did it on an M1 Pro with 16 GB, with qwen3.5:9b, and gave it two real tasks in a small Python package.
- Task A, a one-file
--prefixoption: without--bare, nothing happened for about fifteen minutes. With--bareit passed in 93 s. - Task B, a rename across files: the local model reported the tests green, and an import was broken in both runs (4k and 64k context). On the plan, Claude did it in 38 s with the imports working.
- Ollama gives a machine like ours a 4k context by default.
OLLAMA_CONTEXT_LENGTH=65536raises it.
Verdict: the alias is worth setting up for one-file jobs you can check at a glance. Anything across files waits for the plan, and you always read the diff. The pack gives you the alias block and a one-page rule card.
Sourced synthesis
The setup is three environment variables and a model name. Ollama's guide points Claude Code at http://localhost:11434 with ANTHROPIC_AUTH_TOKEN=ollama s1. Claude Code's docs say ANTHROPIC_BASE_URL overrides the API endpoint s17. Because the variables sit on the alias line, plain claude keeps using your plan.
A set API key replaces your subscription. Claude Code's docs say a set ANTHROPIC_API_KEY is used instead of your subscription even when you are logged in s17. That is why the variables belong on the alias and not in your shell profile.
The model has to fit in memory. qwen3.5:9b is listed at 6.6 GB to 7.6 GB with a 256K context and tool support s18. On our machine ollama ps showed 7.6 GB at the default context and 7.7 GB at 65536.
The context default is the first trap. Ollama uses a 4k context under 24 GiB of VRAM, and says coding tools should be set to at least 64000 s3. You raise it with OLLAMA_CONTEXT_LENGTH when you start the server, and ollama ps shows whether it took s3. Ollama's guide also asks for a 64k or larger window s1.
--bare is the second. One write-up measured the default first request at 14.0k tokens and --bare at 0.75k s8. We did not count tokens. What we saw: the same task at the same 4k context went from nothing in 900 s to a pass in 93 s. --bare also skips hooks, plugins, memory and CLAUDE.md, so the local model never sees your project rules. In our Claude Code 2.1.294, its help says authentication is strictly ANTHROPIC_API_KEY or apiKeyHelper, so the alias sets ANTHROPIC_API_KEY=ollama. We did not test an empty key under --bare.
A header setting is in the alias, but we did not measure it. Unsloth's docs say the attribution header invalidates the local cache and that CLAUDE_CODE_ATTRIBUTION_HEADER=0 fixes it s5, and a Claude Code issue traces the same cache misses s9. --bare already skips attribution, so we cannot say what the extra variable did on our runs.
Small local models often fail on the tool contract, not the code. One benchmark found models calling write_file instead of Write s7. Ollama's issue tracker has a report on truncated and dropped tool calls s10. We did not see a tool-name error. Our failure was quieter: a rename that left an import broken.
First turns can be slow. A field report on qwen3.5:9b with a 64k context, on a 32 GB laptop, saw a first request over 10 min and later turns of 1 to 3 min s6. Our times are in the table below.
What the endpoint cannot do. Ollama's Anthropic compatibility page lists prompt caching and forced tool choice as unsupported s2. Anthropic's docs say it does not support routing Claude Code to non-Claude models through any gateway s4. One critical review reaches the same place we did: local for routine work only s11.
A plain alias beats a router that reuses your subscription token. One setup guide reports those routers failing with a 401 s13.
Code
claude-alt.zsh
# claude-alt: one-file jobs I can check in a glance
# plan: anything across files, or that I can't check by reading the diff
# always: read the diff before committing
alias claude-alt='CLAUDE_CODE_ATTRIBUTION_HEADER=0 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY=ollama ANTHROPIC_BASE_URL=http://localhost:11434 claude --bare --model qwen3.5:9b'
: the alias block with the switch rule as three comments above it. Paste it into `~/.zshrc`, then run `source ~/.zshrc`.
- switch-rule.md
# The switch rule
Keep this next to the claude-alt alias. Decide before your limit hits, not while you wait.
## Goes to claude-alt (local model)
- A job in one file that you can check at a glance
- Adding an option, explaining a function, testing one function
- Work where a wrong result is visible in one short diff
## Waits for the plan
- Anything that crosses files, such as a rename with callers in other modules
- Anything you cannot check by reading the diff
- Work that depends on your hooks or your project rules (the alias runs with --bare, which skips both)
- Long sessions
## Always
- Read the diff before you commit, whichever model wrote it
- After a rename, import the old names by hand. A green test run only covers what the tests cover
## Why this rule
Two tasks on a 16 GB M1 Pro with qwen3.5:9b:
- One file (add a --prefix option): passed in 93 s with --bare.
- Across files (rename a function, keep the old name as an alias): the model said the tests were green, and an import was broken in both runs.
- On the plan, Claude did the rename in 38 s with both the old and the new imports working.
Two tasks on one small package is a starting point. Widen the rule with your own runs.
: a one-page rule card for which jobs go local, which wait for the plan, and the check you always run.
- run-task.sh
#!/bin/zsh
# Usage: run-task.sh <name> <A|B> <plan|alt|alt-nobare> [timeout_s]
# Runs one task on a fresh copy of slugkit and records time, output, diff and the check.
set -u
NAME=$1; TASK=$2; MODE=$3; TO=${4:-1800}
SRC=${SLUGKIT:?set SLUGKIT to the slugkit repo}
OUT=${RUNS:?set RUNS to the output dir}/$NAME
rm -rf $OUT; mkdir -p $OUT; cp -R $SRC $OUT/repo; cd $OUT/repo
A='In slugkit/cli.py add a --prefix option: when given, the slug starts with the prefix and the separator (python3 -m slugkit --prefix blog Hello World prints blog-hello-world). Then run that command to check it.'
B='Rename the function smart_truncate to truncate_words everywhere in this repo (definition, imports, calls, tests, __all__). Keep smart_truncate as an alias of truncate_words in slugkit/truncate.py so old imports still work. Then run python3 -m unittest discover tests and make sure every test passes.'
[[ $TASK == A ]] && P=$A || P=$B
TOOLS=(--allowedTools "Read" "Edit" "Write" "Glob" "Grep" "Bash(python3:*)" "Bash(grep:*)" "Bash(ls:*)")
start=$(date +%s)
case $MODE in
plan) perl -e 'alarm shift; exec @ARGV' $TO claude -p "$P" --output-format json $TOOLS > $OUT/out.json 2> $OUT/err.log ;;
alt) CLAUDE_CODE_ATTRIBUTION_HEADER=0 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY=ollama ANTHROPIC_BASE_URL=http://localhost:11434 \
perl -e 'alarm shift; exec @ARGV' $TO claude --bare --model qwen3.5:9b -p "$P" --output-format json $TOOLS > $OUT/out.json 2> $OUT/err.log ;;
alt-nobare) CLAUDE_CODE_ATTRIBUTION_HEADER=0 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY="" ANTHROPIC_BASE_URL=http://localhost:11434 \
perl -e 'alarm shift; exec @ARGV' $TO claude --model qwen3.5:9b -p "$P" --output-format json $TOOLS > $OUT/out.json 2> $OUT/err.log ;;
esac
rc=$?; end=$(date +%s)
git diff --stat > $OUT/diff.stat; git diff > $OUT/diff.patch
if [[ $TASK == A ]]; then python3 -m slugkit --prefix blog Hello World > $OUT/check.txt 2>&1; else python3 -m unittest discover tests > $OUT/check.txt 2>&1; grep -rn smart_truncate slugkit tests >> $OUT/check.txt; fi
echo "{\"name\":\"$NAME\",\"task\":\"$TASK\",\"mode\":\"$MODE\",\"rc\":$rc,\"seconds\":$((end-start))}" > $OUT/meta.json
cat $OUT/meta.json; tail -3 $OUT/check.txt
: the script we used to time task A or task B on the plan or on the alias, and to save the diff and the check. Set `SLUGKIT` to your test repo and `RUNS` to an output folder first.Do this Monday
- Run
ollama --versionandclaude --version. We used Ollama 0.40.13 s19 and Claude Code 2.1.294. - Pull a model that fits your memory. For 16 GB, we used
qwen3.5:9b, which is 7.6 GB. - Start Ollama with
OLLAMA_CONTEXT_LENGTH=65536 ollama serve, then runollama psand read the CONTEXT column. - Paste the block from
claude-alt.zshinto~/.zshrcand runsource ~/.zshrc. Check that plainclaudestill uses your plan. - Pick a one-file task in a throwaway copy of a repo and run it with
claude-alt. - Read the diff and run the check yourself. Do not rely on the model's own report.
- Try one multi-file rename in the same copy. Import the old names by hand afterwards.
- Keep
switch-rule.mdnext to the alias and edit it with what your own runs show.
Measurements
Machine: MacBook Pro M1 Pro, 16 GB, Claude Code 2.1.294, Ollama 0.40.13, qwen3.5:9b (7.6 GB). The repo was a small 4-module Python package with 12 unittest tests, written for this test. The machine was under memory pressure the whole time, so local times are an upper bound.
| Task | Route | Time | Result |
|---|---|---|---|
| A, add --prefix (one file) | Plan, Claude | 21 s | Passed, blog-hello-world |
| A, add --prefix (one file) | Alias, 4k, no --bare | 900 s | Killed at the 15-minute cap, no edits, no answer |
| A, add --prefix (one file) | Alias, 4k, --bare | 93 s | Passed, blog-hello-world |
| A, add --prefix (one file) | Alias, 64k, --bare | 35 s | Passed, blog-hello-world |
| A, add --prefix (one file) | Alias, 64k, no --bare | not completed | Killed by the system for low memory, no result |
| B, rename across files | Plan, Claude | 38 s | Passed, 12 tests OK, old and new imports work, tests renamed |
| B, rename across files | Alias, 4k, --bare | 464 s | Broke: "All 12 unit tests pass", but from slugkit import truncate_words raised ImportError |
| B, rename across files | Alias, 64k, --bare | 142 s | Broke: 12 tests OK and reported done, but the alias went into __init__.py, so from slugkit.truncate import smart_truncate raised ImportError |
Task B asked to keep smart_truncate as an alias in slugkit/truncate.py. Claude Code also warned that it did not know the model and assumed a 200k-token window. Our sessions stayed far below that, and we did not test a long one.
Go further
What we left out, and where to look if you want it.
- Hosted routes. Providers such as DeepSeek, Z.AI or OpenRouter use the same alias with another base URL. They bill per token and we had no key, so we did not run them.
- Free-tier routers. Routers that pool free tiers promise Claude Code for nothing. We did not test them. Check their terms and where your prompts go before you try one.
- Ollama cloud models. There is no free tier any more, so they are pay-as-you-go.
- Bigger local models. We did not try models in the 26b to 35b range, which do not fit 16 GB with a 64k context. If you have 32 GB or more, a field report ran a 35b model through six graded tasks and got all six, at about 8x the time of the cloud models s12.
- Hybrid routing. Local for subagents and the plan for the main loop is a setup we did not try.
- Other small models. gemma4:e4b is a fallback some guides use. We did not run it.
- A real benchmark. Two tasks on one package show a pattern, not a ranking. To widen the rule, run your own tasks with
run-task.shand keep a table like the one above.
FAQ
Does plain claude change when I add the alias?
No. The variables sit on the alias line, so only claude-alt talks to Ollama.
Why does the alias set ANTHROPIC_API_KEY=ollama and not an empty key?
Under --bare, Claude Code reads its key only from ANTHROPIC_API_KEY or apiKeyHelper. Our alias with ollama as the key worked. We did not test an empty key under --bare to the end.
Is this supported by Anthropic? No. Anthropic's docs say it does not support routing Claude Code to non-Claude models through any gateway s4.
Can I trust a green test run from the local model? Only for what the tests cover. In task B the tests passed while an import was broken, because nothing tested the old import path. Read the diff and import the old names by hand.
Will it be as fast as the plan? Not on our runs. Task B took 38 s on the plan and 142 s to 464 s on the alias, and it still broke.
AIDive