The Third Leash

Fable 5 is back, but it's not in my Max plan — using it spends separate credits. So I made one folder run Claude Code on a different account and the Fable model, interactive and headless. The shim, the token, and why the headless loop is the hard part.

The Third Leash — AI

Last post about the model, I ended on two words: route accordingly. Fable 5 was back, same weights as before, and the only real decision left was where to point it. This is me pointing it — the plumbing version, not the vibe version.

The Fable saga has been a story about leashes. It ships wearing a safety classifier that hands the hard cases back to Opus; then a federal export control froze it for everyone, and eighteen days later that control came off. One leash released, one still on. But there is a third nobody put in a headline — the meter. Fable is not included in the Max plan; running it spends separate, opt-in usage credits, billed at API rates. This post is about who holds that last leash, and how I handed it to a folder.

Here is the situation. I run Claude Code on a personal Max subscription for everything: Opus, max reasoning effort, a stack of settings I have tuned for a year — bypass-permissions by default, auto-approve hooks, effort pinned to the ceiling. That is my whole setup and I do not want to touch it. Fable is genuinely good and I want it — but only on one project, and I want that project's usage on a different account, not mine.

What “route” has to actually mean

Say the requirement out loud and it splits into parts that do not share a mechanism:

  • A different account. Not a different API key — a different subscription, so the usage lands on the right invoice.
  • A different default model — Fable, not Opus.
  • Scoped to one directory tree, and invisible everywhere else.
  • Interactive and headless. My typed sessions and the loop's claude -p children, identically.
  • Reaching into nested git repos, because the loop does its real work three directories deep.
  • Preserving everything else — effort, permissions, hooks, all of it.

Two of these fight each other. “Scoped to one directory” says put it in the project's config. “Reaching into nested repos, including headless” says config won't reach. Resolve that tension and the whole thing falls out.

The one knob for the account

There is exactly one supported way to make Claude Code authenticate as a different subscription without sitting at a login prompt: the CLAUDE_CODE_OAUTH_TOKEN environment variable. You mint it once with claude setup-token, which runs a browser OAuth flow and prints a long-lived token — about a year — bound to whichever account you logged in as. Usage bills to that account. Set the env var and it overrides the credentials sitting in your OS keychain, per process, without disturbing the keychain at all.

That last part matters. It means the switch is ambient and reversible by scope: a process that has the env var is the work account; a process that does not is my personal login from the keychain, exactly as before. No global state, no /login dance, nothing to switch back.

The catch is the precedence chain. Claude Code resolves auth in a fixed order, and an ANTHROPIC_API_KEY in the environment outranks the OAuth token. If one is floating in your shell, your carefully minted subscription token loses to it silently and you are back on the metered API — the exact footgun that eats people running headless agents. So the rule is: set the OAuth token, and make sure no stray API key is set alongside it.

Why the obvious approaches don't work

The instinct is to drop the config into the project's .claude/settings.json. It fails twice.

First, the token cannot live there. A settings env block is applied too late in startup to change which account you authenticate as — you can set ANTHROPIC_MODEL that way and it takes, but not the OAuth token. It has to be a real environment variable, set before the process starts.

Second, and worse, project settings do not traverse into nested git repos. A .claude/settings.json at the project root is invisible to a claude -p whose working directory is a repo nested inside that project. The loop runs its workers deep inside those nested repos. A root-level settings file never sees them. Environment variables, on the other hand, are inherited by child processes down the entire tree — which is the whole reason they reach where settings cannot.

direnv is the next instinct, and it is closer, but it hooks the interactive shell's prompt. It does not fire in a non-interactive or detached bash — which is precisely what an autonomous loop launched with nohup is. Reliable for your typed shell, silent for your automation. Wrong tool for the half that matters most.

And apiKeyHelper? It vends API keys, not subscription OAuth tokens. Wrong currency.

Mechanism Switches the account? Reaches nested repos? Fires for a detached headless run?
settings.json env block no — auth resolves first no no
direnv yes yes no — hooks the interactive prompt
apiKeyHelper no — API keys, not OAuth
PATH shim (this post) yes yes yes

So: not settings, not direnv, not a helper. The switch has to happen at the one place that sees every invocation and can read the current directory — the claude command itself.

The shim

A wrapper named claude, earlier on PATH than the real binary. Every call, it looks at where you are. Inside the project tree, it injects the work account, the Fable model, and re-pins effort, then hands off to the real binary. Everywhere else, it is a straight pass-through.

#!/bin/bash
REAL="$HOME/.local/bin/claude"      # the real launcher; auto-updates transparently
ROOT="$HOME/work/the-project"
case "$PWD/" in
  "$ROOT"/*)
    if [ -r "$HOME/.config/work/oauth-token" ]; then
      export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$HOME/.config/work/oauth-token")"
      unset  ANTHROPIC_API_KEY          # OAuth token must win the auth chain
      export ANTHROPIC_MODEL="claude-fable-5"
      export CLAUDE_CODE_EFFORT_LEVEL="max"
    fi
    ;;
esac
exec "$REAL" "$@"

That is the entire mechanism. A few things it gets right on purpose.

It keys on $PWD at exec time, not on some shell session state. So it does not care whether the caller is my interactive session, the app's own shell shelling out, or a detached loop — they all invoke claude, they all hit the shim, and the shim re-reads the directory every single time. PATH inheritance carries it into every child process, including the ones three repos deep.

It execs the real binary by absolute path, so there is no recursion despite both being named claude, and upgrades to the real installer are transparent.

And it is gated on the token file existing. Until you mint the token, the shim injects nothing and behaves exactly like a stock install. No half-switched state, no window where Fable quietly bills your personal card because the model flipped before the account did. Model and account move together or not at all.

For PATH ordering, the wrapper's directory just has to come before the real binary's. One line in your shell profile prepends it. which claude should resolve to the shim; if it resolves to the real binary you are in an old shell that has not re-read the profile.

The hard part is the headless loop

Everything above is table stakes for the interactive case. The reason this took real thought is the automation.

The project's actual work is the loop I wrote about last time — the thing that files its own tickets and ships them without me in the room. It runs a fresh claude -p per worker, and those workers cd into nested repos to do their edits. That is three separate ways the naive solutions break at once: it is headless, so no interactive shell hook fires; it is detached, so direnv never runs; and it is nested, so project settings never reach it.

The shim handles all three for free, because none of them change the two facts it depends on: the workers call a binary named claude, and their working directory is under the project root. PATH resolves the name to the shim; $PWD is under the root; the token gets injected. The same three-line case statement that catches my keystrokes catches a worker spawned by nohup in a repo the root settings file has never heard of.

One place the shim can't help: the loop passes --model explicitly on its worker commands, and the CLI flag outranks the ANTHROPIC_MODEL environment variable. An env default cannot override an explicit flag — that is the precedence, and it is correct. So the model on those commands has to be changed at the source, from Opus to Fable, directly in the loop's launcher. The account still switches for free via the token; only the explicit model pin needs a hand edit. Precedence is not a bug here; it is the thing telling you where the real switch lives.

And one thing to never do in that project's automation: --bare. Bare mode skips the keychain and the OAuth token — it wants an explicit API key. Run a worker with --bare and the whole account switch evaporates. The loop does not use it; just don't add it.

The carve-outs

Two small decisions separate “works” from “works and isn't wasteful.”

Leave the internal small-fast model alone. Claude Code uses a cheap background model for throwaway housekeeping — summarizing, naming, the stuff you never see. Do not point Fable at it. Set the main model and only the main model; the shim never touches ANTHROPIC_SMALL_FAST_MODEL, so the trivial work stays on the cheap engine where it belongs. “Everything on the premium model” is not the goal; “everything that reasons on the premium model” is.

And effort. My global max-effort pin is an environment variable, so it already survives into every child. The shim re-exports it inside the project anyway — belt and suspenders — so even a bare automation shell that never sourced my profile still runs the workers at the ceiling.

One command to arm it

The only step a human physically has to do is the browser login — you cannot script an OAuth consent. So I wrapped everything else around that single act. One command installs the shim, prepends the PATH entry, runs setup-token inside a pty so the browser flow behaves normally while the token is captured, writes it to a 600 file, then self-verifies by probing through the shim: a headless claude -p from inside the project that comes back on the work account, on Fable, at max effort. A separate re-runnable check does the same probe any time I want to confirm the wiring is still live. Log in as the right account when the browser pops; everything else is automatic.

Verifying the account is the one thing the probe can't fully prove on its own — a headless response looks the same whichever subscription answered. The honest confirmations are /status, which prints the logged-in account, and the billing dashboard, which is the only source of truth about whose credits got spent. Trust the invoice, not the vibes.

Why any of this is worth it

Because Fable at max effort is not cheap, and the whole point was to put that cost on the right account. My verification probe — a claude -p that does nothing but reply “OK” — cost sixty-two cents. One word, max reasoning, sixty-two cents. That is not a complaint; it is the entire justification. A model that expensive running an all-day autonomous loop is exactly the kind of spend you want landing on a clearly labeled account, not smeared across your personal subscription because a shell forgot who it was.

Hot takes

Per-directory identity belongs in the tool, not in your shell profile. Everyone reaches for direnv or a shell function first. Both are session-scoped abstractions bolted onto a per-process problem. The only thing that reliably sees every invocation — interactive, headless, detached, nested — is the executable itself. Shim the binary and the whole class of “it worked in my terminal but not in the cron job” bugs disappears.

The environment variable beat the config file, and that keeps happening. Structured settings feel like the grown-up answer. But config is resolved relative to a directory and does not cross the nested-repo boundary, while environment inherits down the whole process tree. For anything that has to reach a detached grandchild process, the “primitive” mechanism is the robust one. Reach for env before you reach for JSON.

Gate the switch on the credential, not on the location. The tempting version flips the model the moment you cd in. Then you mint the token five minutes later, and for those five minutes Fable was billing the wrong card. Tie every change to the token file's existence and the system has exactly two states — stock, or fully switched — with no embarrassing in-between.

The point

Claude Code has no concept of “this folder is a different me.” It does not need one. Auth is an environment variable, the model is an environment variable, and the working directory is knowable at the moment the command runs — so a dozen lines of shell are enough to make the folder decide the account, the model, and the bill, and to make that decision hold all the way down into a headless loop running in a repo the config never reaches.

Last time I said Fable was back and to route accordingly. This is the router: a directory check and an exec, small enough to fit in a screenful. The folder you are standing in is the account you are spending from. Fable came off two leashes. The third one, I handed to a directory check.

📖
Related reading

Fable 5 Comes Off the Leash — why the model came back: same weights, different leash.

The Loop Files Its Own Work — the autonomous loop this routing has to reach.

The Agent Without a Face — headless mode, and the metered-billing footgun this setup dodges.
💬
Working with a team that wants to adopt AI-native workflows at scale? I help engineering teams build this capability — workflow design, knowledge architecture, team training, and embedded engineering. → AI-Native Engineering Consulting