diff --git a/.claude/worktrees/api-timeout-retry-v2 b/.claude/worktrees/api-timeout-retry-v2
new file mode 160000
index 00000000..9e50cb6e
--- /dev/null
+++ b/.claude/worktrees/api-timeout-retry-v2
@@ -0,0 +1 @@
+Subproject commit 9e50cb6e200b2579e2c51f402bf8a39a82cc1c5c
diff --git a/.claude/worktrees/gitshow-format b/.claude/worktrees/gitshow-format
new file mode 160000
index 00000000..9115b44f
--- /dev/null
+++ b/.claude/worktrees/gitshow-format
@@ -0,0 +1 @@
+Subproject commit 9115b44f2c21745f5619c948b757d28df25c1cb1
diff --git a/.claude/worktrees/provider-config-fallback b/.claude/worktrees/provider-config-fallback
new file mode 160000
index 00000000..67da2dcf
--- /dev/null
+++ b/.claude/worktrees/provider-config-fallback
@@ -0,0 +1 @@
+Subproject commit 67da2dcf75ab6383394ebafb56a21b3c9678c580
diff --git a/.claude/worktrees/wf_616683a9-d46-1 b/.claude/worktrees/wf_616683a9-d46-1
new file mode 160000
index 00000000..77c7d08a
--- /dev/null
+++ b/.claude/worktrees/wf_616683a9-d46-1
@@ -0,0 +1 @@
+Subproject commit 77c7d08ab3ecf01016b0e15319c2aec8583d431e
diff --git a/.claude/worktrees/wf_616683a9-d46-2 b/.claude/worktrees/wf_616683a9-d46-2
new file mode 160000
index 00000000..346772a8
--- /dev/null
+++ b/.claude/worktrees/wf_616683a9-d46-2
@@ -0,0 +1 @@
+Subproject commit 346772a8b309fb5cd0c030049211571288eaeca8
diff --git a/.claude/worktrees/wf_616683a9-d46-3 b/.claude/worktrees/wf_616683a9-d46-3
new file mode 160000
index 00000000..4ba51771
--- /dev/null
+++ b/.claude/worktrees/wf_616683a9-d46-3
@@ -0,0 +1 @@
+Subproject commit 4ba51771e5047b02b6fb851764fe9c7b9541b049
diff --git a/.claude/worktrees/wf_616683a9-d46-5 b/.claude/worktrees/wf_616683a9-d46-5
new file mode 160000
index 00000000..a0e9833f
--- /dev/null
+++ b/.claude/worktrees/wf_616683a9-d46-5
@@ -0,0 +1 @@
+Subproject commit a0e9833ff3b577ecbece599b649c80700ec136f8
diff --git a/.claude/worktrees/wf_616683a9-d46-6 b/.claude/worktrees/wf_616683a9-d46-6
new file mode 160000
index 00000000..2a1d1426
--- /dev/null
+++ b/.claude/worktrees/wf_616683a9-d46-6
@@ -0,0 +1 @@
+Subproject commit 2a1d14269407f271d7b7b73aef61b8683c860ded
diff --git a/.claude/worktrees/wizard-entry-points b/.claude/worktrees/wizard-entry-points
new file mode 160000
index 00000000..d54f0280
--- /dev/null
+++ b/.claude/worktrees/wizard-entry-points
@@ -0,0 +1 @@
+Subproject commit d54f0280b877d02a846d166fb55ba8f38ec2ad3e
diff --git a/docs/pr-3211-plan.md b/docs/pr-3211-plan.md
new file mode 100644
index 00000000..cbe90729
--- /dev/null
+++ b/docs/pr-3211-plan.md
@@ -0,0 +1,177 @@
+# PR #3211 — Execution Plan: 3-Tier Credential Resolution (REVISED)
+
+**Branch**: `worktree-provider-config-fallback`
+**Worktree**: `.claude/worktrees/provider-config-fallback`
+**CI State**: Build ✅ Test ✅ Clippy ✅ Docs ✅ Windows ✅ **Fmt ❌**
+
+## Reviewer Feedback
+
+From `1716775457damn`:
+> "the `from_env_or_saved` naming across multiple provider files could be consolidated
+> into a shared trait or helper to reduce duplication. Also, cargo fmt is failing"
+
+## Investigation Findings (Updated)
+
+### Key discovery: `from_env` vs `from_env_or_saved` are DIFFERENT things
+
+There are **TWO** `AuthSource` methods in `anthropic.rs`:
+- `from_env()` (line 44) — **tiers 1+2 only** (process env + .env), NO stored config
+- `from_env_or_saved()` (line 629) — **tiers 1+2+3** (process env + .env + stored config)
+
+And TWO in `openai_compat.rs`:
+- `from_env(config)` (line 132) — **tiers 1+2 only**
+- `from_env_or_saved(config)` (line 151) — **tiers 1+2+3**
+
+The `_or_saved` suffix is **semantically meaningful** — it distinguishes the 3-tier path from the 2-tier path. Renaming them identically would lose this distinction.
+
+### What IS actually duplicated and safe to consolidate
+
+| Duplication | anthropic.rs | openai_compat.rs | mod.rs (shared) |
+|---|---|---|---|
+| `read_env_non_empty(key)` | ✅ line 773 | ✅ line 1608 | ❌ not shared |
+| `read_base_url()` | ✅ line 798 | ✅ line 1625 | ❌ not shared |
+
+Both `read_base_url()` functions follow the same 3-tier pattern:
+```
+Tier 1: std::env::var(key) → return if non-empty
+Tier 2: dotenv_value(key) → return if non-empty
+Tier 3: read_base_url_from_config(provider_kind) → return if Some
+Default: fallback string
+```
+
+But there's a complication: `anthropic::read_base_url()` hardcodes the env var and provider kind, while `openai_compat::read_base_url(config)` takes them from the config struct. The shared helper needs to parameterize both.
+
+Similarly, `read_env_non_empty` is identical in both files — just reads env + .env fallback.
+
+### Why NOT a shared trait (confirmed)
+
+Anthropic's `AuthSource` has 4 variants (None, ApiKey, BearerToken, ApiKeyAndBearer) while OpenAI-compat has only ApiKey. Different return types → trait adds complexity.
+
+## Execution Plan (Revised)
+
+### Step 1: Extract `read_env_non_empty()` into `mod.rs`
+
+Both files have identical `read_env_non_empty(key)` implementations. Move to `mod.rs` as `pub(crate) fn read_env_non_empty(key: &str) -> Result