When something isn't working

Config that isn't applying, a key that won't resolve, hooks that never fire, a run that does nothing — the checks in the order that finds it fastest.

Most stella problems come down to one of a small number of causes, and nearly all of them can be answered locally in a few seconds. This page is organized by symptom, and inside each section, the cheapest check that could explain it comes first.

The three commands that answer most of it

stella config     # what actually resolved: provider, model, key source, workspace
stella models     # every provider, its key status, and which source supplied it
stella doctor     # is the local state — and the model config — sound?

Run these before you form a theory. stella config in particular checks every source — flags, environment variables, all three settings.json levels, credentials.toml — and prints the actual result, which answers most config mysteries on its own.

"My settings.json isn't applying"

Four checks, in this order.

1. Check you're editing the right file

There are three levels, and they get combined, not just picked from:

LevelPathPriority
project<workspace>/.stella/settings.jsonhighest
org-managedSTELLA_MANAGED_SETTINGS, else /Library/Application Support/stella/settings.json (macOS) or /etc/stella/settings.jsonmiddle
user~/.stella/settings.jsonlowest

A missing file at any level is fine — it just gets skipped. A file that exists but has bad syntax shows up as a named error instead of silently being dropped, so if you're not seeing an error, the file parsed fine.

2. Check for a higher-level override

Most fields combine in this order: project, then org-managed, then user. stella config shows you the final, combined result. If your user-level value is losing, something more specific is also setting it. On a company-managed machine, you may have an org-managed file you didn't know about — check the path in the table above.

3. Check how the field combines

This is the check people skip, and the rules genuinely differ by field:

providers, agent_engine_config

Combine field by field — even agents.<role>.params merges one setting at a time.

allowed_models

Replaces the whole list. Whichever level sets it owns the entire list. A project file listing one model does not add to your user list — it replaces it.

hooks

All combine together. Org, repo, and personal hooks all run, and no level can remove another's hooks.

tools, mcp

The last level wins, per key — and an org-managed off is a ceiling no lower level can turn back on.

4. Check for something outside settings.json

Command-line flags and some environment variables sit above settings.json:

  • Model--model beats STELLA_MODEL, which beats agents.default.model, which beats default_model, which beats auto-detection. The flag and the environment variable fill the same slot, so the flag wins if both are set, and either one wins over any setting. A model set in settings can never silently override a flag or variable.
  • API key--api-key beats the provider's environment variable, which beats settings.json's api_key, which beats credentials.toml, which beats an interactive prompt.
  • Base URL--base-url beats the ZAI_GLM_CODING_PLAN=1 toggle (Z.ai only), which beats settings.json's base_url, which beats the built-in default.

--base-url and STELLA_BASE_URL are not part of the model chain. They just replace the address of whichever provider the chain already picked, without changing that pick or the credential that goes with it. That's how one provider's key can end up being sent to a different provider's address. stella doctor flags this pairing by name.

An environment variable you exported almost always beats the settings file, which explains most "but I set it in settings.json" reports.

Preview a change without starting a run — global flags go first:

stella --model anthropic/claude-fable-5 config

"My API key isn't resolving"

stella models         # every provider, key status, and the source that supplied it
stella auth list      # what credentials.toml holds, redacted, with resolution source

The chain is first match wins, and nothing after the first match is ever read:

  1. --api-key (needs an explicit --model provider/…)
  2. the provider's environment variable
  3. settings.json under providers.<id>.api_key
  4. ~/.stella/credentials.toml
  5. an interactive prompt, in a terminal, once you've named a provider

Three specific traps:

The key is only in a .env file your shell never loads

Project dotenv files load into the environment before step 2, so a repo's .env.local does reach stella. But an exported shell value always wins, and a dotenv file loaded by an interactive-shell hook doesn't load for a non-interactive command. stella auth set <provider> is the fix that sticks.

api_key_env renamed the variable

A providers.<id>.api_key_env setting renames the main variable — the named one gets checked first, and the provider's usual variable becomes a fallback. If a teammate committed that setting, your usual variable is now second in line.

An empty string is not unset

providers.<id>.api_key: "" counts as unset. But a value that's just whitespace, or an old expired key, still resolves and then fails at the provider. A 401 error means the chain worked and the key itself is wrong.

A 401 that names the wrong provider

A base URL doesn't select which provider is used. If --base-url, STELLA_BASE_URL, or a settings.json base_url points at one provider's address while the model chain resolved a different provider, that other provider's key gets sent to the first provider's address. Whichever provider received the request reports the error, so the error message blames the wrong side.

stella doctor catches this mismatch and names both sides:

  ✗ model config — 1 model setting(s) name something the provider will reject
      --base-url / STELLA_BASE_URL: `https://api.z.ai/api/paas/v4` — points at
      Z.ai (GLM 5.2) (api.z.ai) while the resolved provider is OpenRouter — your
      OPENROUTER_API_KEY credential would be sent to Z.ai (GLM 5.2) …

The fix is to make both sides agree: pin the provider to match the address (--model zai/glm-5.2), or remove the override. A host stella doesn't recognize is left alone — using this flag for a private proxy or gateway is a normal use case.

Auto-detection picked a provider you didn't expect

With no --model set, stella picks the first provider with a working credential, checked in this order:

openrouter, zai, anthropic, openai, xai, deepseek, gemini, then vertex, and bedrock LAST

OpenRouter comes first because nobody sets OPENROUTER_API_KEY by accident — having one means you deliberately chose it for routing. Vertex and Bedrock come last so that generic AWS or Google credentials left over from something else never get picked automatically. Any leftover key earlier in that list wins over the one you actually meant to use — run unset on it, or pin --model yourself. stella doctor prints which provider and model the next run would pick, and why.

"Hooks, MCP servers, or custom tools never load"

This is almost always the project trust boundary, working as intended.

A fresh clone's hooks, providers.*.base_url / api_key / api_key_env, mcp.registry_url, context_providers, per-agent prompt settings, and the whole .stella/mcp.toml file are all held back until you opt in. Otherwise, any repository you cloned could run commands on your machine or route your keys through its own server.

A message on stderr names everything that got skipped. To allow it:

export STELLA_TRUST_PROJECT=1   # scope it per-repo with direnv or a shell guard

STELLA_PROJECT_HOOKS=1 is a narrower, older version of this — it trusts hooks, MCP, and context providers, but still blocks credential routing.

An org-managed denial is a ceiling: tools: { "web": "off" } or authority.project_custom_tools: "off" set at the org-managed level can never be turned back on by a project file, or by STELLA_TRUST_PROJECT=1. If a capability stays off after trusting the project, check the org-managed file.

"The model I configured isn't the one that ran"

Check allowed_models first

It replaces the whole list, not just adds to it. A model missing from the final list can't be selected, no matter how it was configured.

A role fell back

A role assigned to a model whose provider has no working credential falls back to the session's main model, with a notice, rather than causing an error. A working configuration can never turn a runnable session into a broken one, so read the notice to see what happened.

A circuit breaker opened

Three connection failures in a row for a provider open a breaker for it, and routing skips that provider, reporting the fallback clearly. After a cooldown period, one test call decides whether it closes again.

Cross-family judging moved the verifier

The verifier prefers a healthy provider running a different model family than the worker. With only one family configured, it falls back to the same family, with a warning, never an error.

"effort or reasoning seems to be ignored"

Two separate causes, easy to mix up:

  1. The provider drops the setting entirely. Z.ai, DeepSeek, and local endpoints never send effort at all. The setting records what you wanted, but changes nothing. For those, tell the roles apart with a specific prompt and params.max_tokens instead.
  2. An automatic setting is overriding you. effort_auto and reasoning_auto override the per-agent effort and reasoning settings. If you want your per-agent values to apply, turn the matching automatic setting "off".

That second point is why the balanced profile has no agents block at all, and the maximum-quality profile turns both automatic settings off.

"The agent answered about code that isn't there"

The code map rebuilds itself on every query, so a stale answer usually means the update pass failed, not that it never ran. That failure is reported, not hidden:

warning: the code graph index pass failed — answering from what the index already
holds, which may be stale

If you see that, rebuild it directly:

stella init
stella search "where is the_symbol_you_expected defined"

A symbol that genuinely has two definitions gets listed as two results instead of being silently picked for you — that's not a bug, and it's often the more useful finding.

"A session won't open, or a command mentions store.db"

stella doctor            # read-only diagnosis, always safe
stella doctor --repair   # quarantine a store SQLite judged corrupt

--repair renames files, it never deletes them. The database and both side files get moved aside together, under one timestamp. Whatever is still readable gets copied into a separate recovered database, and your next session starts fresh. Every quarantine can be undone with mv.

doctor runs before settings are loaded, on purpose — a workspace with a corrupted store needs to stay diagnosable even without a working model config. It has no JSON output. --output-format is a flag for run and fleet, and doctor doesn't accept it.

"The run ended without doing anything"

A verification plugin stopped it

A plugin driving the turn can end a run before it edits anything — for example, if the change is bigger than some threshold. The rule belongs to that plugin, so check its manifest for what it refuses to do. There is no built-in scope check in stella itself, and the agent_engine_config.headless_scope_bypass setting does nothing — it's not what ended your run.

The spend limit tripped

--spend-limit stops the run cleanly, between steps. Exit code 1, and already-committed work stays. Check cost_usd from --output-format json to confirm.

Verification found nothing to verify

A run that changed nothing can never claim success, by design. If the task was already done, that's actually the correct outcome.

It was interrupted

Exit code 130 means SIGINT, and 143 means SIGTERM — distinguishable from a real failure, so a wrapping script can treat them differently.

"Runs cost more than they should"

Start with the cache columns — they explain most surprises:

stella stats --format json |
  jq '.rows[] | select(.cache_hit_rate < 0.2) | {provider, model, cache_hit_rate, cache_expired_rewrites}'

Then find out what changed between turns, since that's usually what caused a cold cache:

stella inspect <EXEC> --step 0 --diff --only system

Understand what a run cost walks through that whole process.

Working entirely offline

If you're troubleshooting on a plane or somewhere with no network, this much needs no connection and no key at all: stella config, stella doctor, stella models (listing), stella scripts, stella storage, stella commands, stella stats, stella usage, stella inspect, stella context, stella proposals, stella memory, and stella observe. stella search also works with no key configured — it falls back to matching symbol names and scanning files instead of ranking by meaning. If you have an embedding model configured, it will make network calls to embed your query and any files still waiting to be indexed.

The one remaining automatic network call is the model-catalog refresh, which happens at most once every 24 hours:

export STELLA_CATALOG_AUTO_REFRESH=0

Still stuck

Collect the three command outputs from the top of this page, plus the command that's failing, and open an issue. stella config hides your key behind a preview, so its output is safe to paste. stella inspect output is not safe to paste — it contains your prompts and source code, and records never leave your machine unless you move them yourself.