Contents

AI direction through the person's own Claude, with no API key

Status: built, 2026-10-14 (session protocol 1.17).

The owner's ask, 2026-10-14: AI direction did not work without an Anthropic API key. It must work over MCP like everything else, through the person's own Claude (claude.ai's connector, Claude Code, or the Claude plan through the bridge), with no key anywhere. A key stays an option, never a requirement.

What refused without a key (the audit)

Path Without a key Through the connected Claude
The AI director (direct_episode, studio/tools/direct.ts) director.no_model, even when the caller was Claude itself over MCP; Studio quietly cut it with the heuristic director, and on the desktop a key-less model ran and every scene fell back Built here: the calling Claude directs, a task at a time; Studio's Direct goes to the person's own Claude first (Claude Code on their plan, then a Claude connected now), then a runner or key, else waits; never quiet about which
reelwright direct (cli/direct.ts) needs a key unless --heuristic Built: the Claude Code here on the plan first, before any key (From the command line, below)
The showrunner chat dry runs only on the desktop unless serve --claude-plan; the web needs a key, the bridge or a runner claude-easy's chat first-run ("Use my Claude")
Writing with Claude (assist.write, studio/assist.ts) assist.no_model, even with plan chat on Built: Claude Code on the plan writes, before any key; over MCP the caller writes into the script itself (write_screenplay)
Memory after directing (studio/tools/memory.ts) the model pass is skipped Built: an MCP caller does memory's model pass itself; Studio's pass runs on Claude Code on the plan first; without Claude it says so
Look directions (studio/tools/styles.ts) heuristic directions, quietly Built: an MCP caller proposes them itself; Studio's go to Claude Code on the plan first; without Claude they say so
eval, fixtures record a key by design: they measure the API model Built for eval record: the set recorded on the plan, before any key (From the command line); eval release checks a keyed call, and fixtures record the API's requests, so both keep the key

How Claude directs over MCP

A Claude that reaches the show over MCP is a model already. So when it calls direct_episode (through reelwright mcp on this computer, or the web service's /mcp), the call runs under that client's name (claude-easy-3's runThrough, protocol 1.15), and the director runs with the caller as its model (callerRun, claude/plan-run.ts):

  • The call answers at once with the first task. The task holds the director's brief, the tool that answers it with that tool's input schema, and the scene. It is the same PlanRun that a run on the plan through the person's runner uses: every call the director would make of the model becomes a task.
  • The caller answers with plan_submit (the tool the task names and its whole input, an object; no run id needed for its own run). Each answer is judged exactly as the API's would be: schema, names, assembly, validation, provisional compile and film grammar. The answer to plan_submit is the next task: what to fix (pointer, message, hint), the next scene's coverage, or the next scene.
  • The last answer names the proposal ((change chg.…)), which waits for the person in Studio, author showrunner, via the client. Its provenance says "Claude, through claude-code".
  • plan_task and plan_submit are listed for every MCP client on both servers (desktop and web), and Claude Code keeps them in view. Studio's own chat never has them.
  • One run at a time for each caller. A new direction cancels one it left unfinished: its signal fires, so nothing is proposed for it.
  • A task waits 15 minutes for its answer, as a runner's does.
  • No key is read and no model is called from Reelwright. heuristic: true still directs with no model at all.

How Studio's Direct with Claude finds a Claude

When the show's director is the model director and the daemon speaks 1.17, Studio sends direct_episode with waitForClaude: true. The daemon takes the person's own Claude first, before any key (the owner's rule):

  1. Claude Code on this computer, on the person's plan, whenever it is installed and signed in with a Claude plan (claude/plan-agent.ts). It doesn't depend on serve --claude-plan: every desktop daemon has it, and none on the web service. The daemon leaves the direction waiting (below), then runs one turn of that Claude Code. That turn:

    • can call only direct_episode, plan_task and plan_submit;
    • has no key in its environment;
    • has a prompt of its own, beside the chat's;
    • reaches the show through reelwright mcp --studio-work: Studio's own work, not an AI from outside, so no presence file, but its calls run under its client's name, which its run is keyed by and its proposal keeps (via). The chat's --studio-chat names no client, so a direction there would find no run of its own (plan.no_run).

    It takes the waiting direction through Reelwright's own MCP server and directs it as above. Studio is answered with what it proposed. The prompt file is direct_episode-prompt.md.

  2. A Claude connected to the show over MCP right now (claude-easy-3's session.agents: the service's link for claude.ai or Claude Code, or reelwright mcp on this computer), for the person asking. The direction waits for it, and the key is not used. The answer says which Claude is connected (waiting.claude) and whether a key could do it (waiting.key).

  3. A model that can answer now: the person's runner on their plan (the web service), or a key. A model with no key (ready() says why) counts as none: it is never run to fall back scene by scene.

  4. Otherwise it waits for whichever Claude connects.

"Direct with your API key" (withKey) skips 1 and 2 when the person asks for it.

A waiting direction is kept in the show as .reelwright/studio/asks/<id>.json (studio/asks.ts), so a reelwright mcp in another process sees it too. The script editor's bar:

  • says the direction waits for Claude, naming the connected Claude when there is one;
  • says what to ask it: "Direct the episode waiting in Reelwright." (Copy what to say);
  • offers Direct without Claude (the heuristic director, free);
  • offers Direct with your API key when a key could do it instead.

The connected Claude finds the direction in studio_context, which lists what waits with the exact call that takes it (direct_episode with its ask id). Taken, the ask carries what Studio asked for (the script's text, scenes, note, rationale). It stays in the show as being directed (<id>.taking.json, naming the client and its process) until its run ends.

The daemon reads the asks folder each second and tells Studio (session.asks, protocol 1.17):

  • what waits;
  • which Claude is directing what.

The bar, and the line under the script, follow it:

  • "Claude (claude.ai) is directing this" while a Claude directs;
  • that it stopped with no proposal, if it ends without one;
  • when Claude's proposal arrives (change.event), the bar goes, the result says so, and the way to the timeline opens.

One ask waits per script, the newest. A direction of that script by any other way ends it. A taken ask whose process is gone is dropped.

A Studio before 1.17 never sends waitForClaude, gets what it got before (director.no_model, then the heuristic), and reads past session.asks. Show settings say what Direct uses as it really will be.

Memory, looks and writing through the same Claude

The director's way is every AI part's way (director-over-mcp-2):

  • Over MCP, the caller is the model. remember_episode and propose_styles, called by a Claude over MCP, answer at once with the model's first task, as direct_episode does (callerWorks, claude/plan-run.ts). Memory's model pass reads each episode a task at a time; the style lab's directions are one task. Each answer is checked as the API's would be. The proposal says which Claude did it: memory's entries name "Claude, through claude-ai", and the looks' summary says "Claude, through claude-ai proposed 2 of them". Nothing is charged. model: false keeps the old way: no model at all.
  • From Studio, Claude Code on the person's plan first (claude/plan-agent.ts), before any key:
    • The memory pass after directing (and remember_episode from Studio) is one turn of that Claude Code calling remember_episode. Memory's apply: auto approves what it proposed. With the model pass off in memory's settings, Claude Code is not asked.
    • The style lab's directions are one turn calling propose_styles.
    • Writing with Claude is a chat of its own with no tools: the writer's instructions and the script as its prompt (writer-prompt.md), each call a turn, its text the suggestion, a revision heard with what came before. The writing panel shows "Claude Code" and no price, and Studio offers writing (assist in the hello) when that Claude Code is ready.
  • When the plan cannot, the pass or the directions go on with a key or without Claude, and say why: "Claude Code on your plan could not read it (…)", "…could not propose them (…), so Reelwright's own rules drew them."
  • On the web service there is no Claude Code of the person's: a key, the person's runner for the memory pass, or Reelwright's own; the connected Claude does any of it when asked. Writing without a key says to ask the Claude connected to the show, which writes into the script itself.

From the command line

reelwright direct and reelwright eval record keep their director in their own process, as with a key, and give its model's part to the Claude Code here when it is signed in with a Claude plan (claude/plan-local.ts), before any key; --with-key takes the key instead.

  • The run and its server. Each call the director would make of the model is a task of a run (PlanRun, as Studio's). The command starts a small MCP server of its own on this computer's loopback (127.0.0.1, a port of the system's choosing) with only the run's two tools, plan_task and plan_submit, answering only a bearer token made for it. Claude Code is started for one turn with that server as its only one (--strict-mcp-config, an http entry whose header names ${REELWRIGHT_MCP_TOKEN}, the token in its environment alone, never in an argument or a file), its own tools off and only those two allowed, in the show's folder, with no API key in its environment. Its prompt tells it to take tasks until an answer says the run is done. A slow answer says it is still coming every 25 s, so no client gives up on it.
  • The end. Once directed, the command ends the run: the answer Claude Code waits for says it is done, it ends its turn, and the server stops; one that does not end in two minutes is stopped. Claude Code ending its turn before the work is done fails the call waiting and the rest at once: the director leaves them to the heuristic, saying why.
  • What it says. "Directing on Claude Code on your Claude plan…", each scene's calls with no price, the total "on your Claude plan", and the episode's provenance names Claude Code. A dry run says it would direct on the plan, and starts nothing.
  • The evaluation set. Each script is directed in a Claude Code session of its own, as Studio directs an episode, its calls written down for replay as the API's are (the requests are the director's, whoever answers). Each ask is put to Claude Code's chat on its own copy of the demo (PlanChat), as Studio's chat is on the plan; what came of it (outcome, tools, proposals and their contents) is kept in asks.json, since that chat has no requests of Reelwright's to replay. eval.json says "on": "plan" and which Claude Code ("Claude Code 2.1.301 on a max plan"); the evaluation replays the scripts with no key and judges the asks as they came.

Never quiet about it

Wherever Studio goes without Claude, it says so in plain words, with how to have Claude do it:

  • A heuristic cut says "Directed without Claude, by the heuristic director: free", then why and what to do:
    • Show settings chose the heuristic director: choose Claude there;
    • no Claude could direct it: connect Claude;
    • the person chose Direct without Claude: nothing more to say.
  • A direction's summary starts with who directed it, and with how many scenes fell back to the heuristic director and why:
    • Claude through claude-code;
    • Claude Code on a plan;
    • the model on the person's key.
  • Look directions Reelwright suggested alone say they are its own, without Claude.
  • A memory pass without Claude says Claude did not read the episodes, and how to have it.

Proof

  • Core (core/test/director-over-mcp.test.ts, plan-agent.test.ts, host-direct-mcp.test.ts, memory-looks-over-mcp.test.ts, memory-studio.test.ts, assist.test.ts):
    • A scripted MCP client named as Claude Code directs the salvage claim with the hand-written drafts: no key, the tasks whole, a bad draft sent back with what to fix, the proposal via the client with Claude's provenance.
    • A new direction takes the place of an unfinished one.
    • heuristic: true still runs with no run.
    • Studio's direction waits, the connected Claude finds it, takes it and directs it.
    • A key-less model counts as none.
    • Claude Code on the plan goes first, with what Studio asked for.
    • Through a real daemon (director-routing.test.ts), with a key there and claude.ai connected, claude.ai directs and the key is never called, and Studio hears the direction wait, then which Claude is directing it, then its proposal. With no Claude connected, the key directs. "With your key" uses the key though a Claude is there.
    • A direction another way ends the wait.
    • The fake Claude Code directs through a real reelwright mcp over stdio, and through it reads an episode for memory, proposes a style's directions, and writes for the writing panel; with no proposal of its, the looks say so and Reelwright's own are drawn.
    • claude.ai over MCP reads an episode for memory and proposes looks itself, its name on both.
    • Studio's memory pass goes to Claude Code on the plan first, says so when it cannot, and asks nothing of it with the model pass off; with apply: auto, what it proposed is applied.
    • Over the web service's /mcp, claude.ai reads an episode for memory itself and Studio hears the proposal as it is made.
    • The web service's /mcp, with no key and no runner: a claude-ai client directs; Studio's waiting direction is taken; the proposals reach Studio live.
  • Engine (engine/tests/ui/rw_script_editor_test.gd, rw_show_settings_test.gd, rw_session_client_test.gd):
    • Direct with Claude at 1.17 asks with waitForClaude, shows the bar, and shows Claude's proposal as it comes.
    • Direct without Claude cuts it free.
    • A daemon before 1.17 is never asked to wait.
  • A web check (tools/web/director-over-mcp-check.ts): the web edition with no key, a scripted claude.ai (the SDK's own client) connected to the show:
    • Direct gives the direction to it, and the bar says so;
    • it takes the direction: "Claude (claude.ai) is directing this", over the script and under it;
    • it directs, and Studio shows the proposal.
  • Mutations, one per guarantee (core/test/director-over-mcp.test.ts names them).