Auto mode classifier denies writing a new README in a local repo the user asked for, no reason or in-line approval

Status Open
Reported on v2.1.296
Maintainer reply None cached
Activity 0 comments · opened Oct 11, 2026

What happened

In auto mode the classifier denied a Write of a new README.md in a local repo that the agent had created a few minutes earlier, in the same session, at my request. It gave no reason ("judged this action dangerous (it gave no explanation)") and offered no way to approve it in-line. The task stopped, and I had to switch to bypass permissions to finish.

Context: I asked Claude to build a small reproduction repo for a bug. It scaffolded the repo (git init, package.json, src/worker.ts, test.mjs) and ran the tests, all allowed. The last step, writing the README with the findings, was denied. Nothing in the action was destructive, remote or outside the working tree: a new Markdown file in a new local repo.

The calls

Allowed, in this order, earlier in the same session:

mkdir -p ~/git/mikepage/effect-mcp-workers-repro/src && cd ~/git/mikepage/effect-mcp-workers-repro && git init -q -b main
# Write package.json, wrangler.jsonc and .gitignore (heredocs in Bash)
npm install
# Write src/worker.ts
# Write test.mjs
npm test   # starts wrangler dev per scenario

Denied:

Write
  file_path: /Users/mikepage/git/mikepage/effect-mcp-workers-repro/README.md
  content:   (below, 102 lines of Markdown)

Denial text:

Permission for this action was denied by the Claude Code auto mode classifier. Reason: The server-side auto mode classifier judged this action dangerous (it gave no explanation).

After switching to bypass permissions, the same Write with the same content succeeded.

<details><summary>README.md content that was denied</summary>

````markdown

effect-mcp-workers-repro

Effect 4 on Cloudflare Workers: a long-lived fiber that more than one request
uses hangs. Written up after
recranet/console dropped its Effect
McpServer for a CLI (October 2026).

Run

npm install
npm test

Each scenario gets a fresh wrangler dev isolate. Locally workerd detects
the hang and answers 500 ("would never generate a response"); in production
the request just sits until the client gives up, logged as canceled with
~1 ms CPU.

Result (effect 4.0.0, wrangler 4.147.0)

FAIL fiber: a second request uses the long-lived fiber
       GET /fiber/wait     expected 200  got 200
       GET /fiber/wait     expected 200  got hang
FAIL fiber: first request answers before the fiber has run
       GET /fiber/nowait   expected 200  got 200
       GET /fiber/wait     expected 200  got hang
ok   mcp: first request is tools/list (control)
ok   mcp: first request is a GET (405, never touches the server fiber)
ok   mcp: first tools/list is cancelled by the client

The minimal McpServer here does not hang. The console's did, on the same
Effect version, with more around it: two servers built with Layer.fresh,
four protocol revisions, auth middleware and per-request services, ~70
tools. The mechanism below is what the console found and what the fiber
scenarios show.

Mechanism

  • A fiber has one dispatcher (Scheduler.ts, MixedSchedulerDispatcher).

It queues work behind a single setImmediate/setTimeout(0) and sets
running until that timer fires.

  • On Workers a timer belongs to the request that set it. When that request

finishes or is cancelled, the timer never fires.

  • A long-lived fiber (a queue consumer, McpServer's RPC server, anything

forkScoped in a layer) is driven by whichever request happens to touch
it. If its pending timer was set by a request that has since ended,
running stays set, no new timer is ever scheduled, and every later
request that waits on the fiber hangs until the isolate is recycled.

McpServer.layerHttp forks its RPC server with Effect.forkScoped, so it
has exactly this shape. The console worked around it with a warm-up: one
tools/list per endpoint before the isolate's first /mcp request, with a
10 s bound on waiting for another request's warm-up.

Removing MCP didn't fix the scheduler. It removed the only fiber that
outlived a request: every /api request runs in its own fiber, which starts
and finishes inside that request.

The same class of bug, without fibers

Both hit the console on the same days and look identical in Observability:
every request on one isolate hangs, /api/health and static pages still
answer.

  • A promise one request starts and others await. A Workers request

can't settle a promise another request started. jose's
createRemoteJWKSet, kept per isolate, shares one pending key fetch; when
the request that began it was cancelled, every later request waited
forever. The console's app start (makeHandler plus the MCP warm-up) was
the same pattern. Fix: keep only settled values per isolate, never a
pending promise; build the router at module load.

  • Startup CPU. Workers allow 1 s of CPU for global scope. Each

module-level Effect.fn("name") captures its definition stack with
new Error(), which is expensive in workerd: ~800 of them cost ~200 ms.
The console sets Error.stackTraceLimit = 0 while its modules load and
restores it after. Building McpServer and its toolkits on every isolate,
also those that never served MCP, added to cold-start CPU.

For the next Effect project with an MCP server

  • Don't put a long-lived fiber behind a Worker's fetch handler. Put the MCP

server in a Durable Object, or keep every tool call inside the request's
own fiber.

  • Or skip MCP: a CLI generated from the HttpApi contract

(HttpApi.reflect) gave every endpoint as a command with the web app's
auth and roles, and nothing extra to run.

  • Give MCP its own Worker if it stays, so a stuck isolate can't take the UI

API with it and the UI doesn't pay its startup.

  • Never store a pending promise per isolate. Cache results, not fetches.
  • Run wrangler check startup in CI and watch startup CPU as the codebase

grows.

  • Add a per-request time limit that answers 503 and logs what it waited on,

so a hang shows up as an error instead of a silent canceled.

References

recranet/console (private): issues #22, #26, #27; commits dcfb80f
(Access keys), 33a82c7 (module-load router, bounded MCP warm-up),
aa7435c (startup CPU), a3a7bba (MCP replaced by the CLI).

````

</details>

Expected

  • Routine local file writes that the user asked for are allowed, or
  • a denial comes with a reason and an in-line "allow this once" prompt, as in default mode.

Impact

This is not a one-off. Auto mode regularly blocks simple tasks like this, so it isn't workable for day-to-day use and I fall back to bypass mode, which is less safe than a prompt would be.

Related: #101202, #98109, #100031, #99911, #98598.

Environment

  • Claude Code 2.1.296, desktop app (Code tab), macOS (Darwin 25.6.0)
  • Permission mode: auto
  • Tool: Write, new file in the working tree

🤖 Generated with Claude Code

View original on GitHub ↗