DEV Community

Dekita
Dekita

Posted on Originally published at dekita-ai.pages.dev

Your unattended agent run dies at the permission prompt. This release closes that path.

A long agent run that starts with no one at the terminal dies in one place far more often than in the model. It dies at the permission prompt. The agent reaches a tool call that asks for confirmation, nobody is there to confirm it, and the session waits for a key press that will never come. The run is not wrong. It is parked.

Claude Code 2.1.319, released on November 6, 2026, gives you a switch for exactly this. With permissions.denyInsteadOfPrompting set, a call that would stop and wait is denied instead, and the turn carries on. The setting applies in terminal sessions, in subagents, and in agent teams. In other words, the run that used to freeze now finishes, and it finishes in a defined direction.

That is the whole appeal, and it is also where the trap lives. Deny-instead-of-prompt removes the stall. It does not, by itself, tell you that a deny happened, which tool was denied, or how many in a row. A run that fails closed looks exactly like a run that did less work. Your CI went green, the log is shorter than expected, and the missing step is a tool call the policy refused and nobody read about.

So the acceptance test for an unattended run is not "it did not hang." It is "its denials are countable, attributable to a tool, and visible in the artifact." The rest of this post is the setting, the three ways the run can still go quiet even with the setting on, and a short script that turns a transcript into a pass/fail on that test.

The setting, pasted

Put this in the settings that your unattended run actually reads (the machine that runs the job, not your laptop):

{
  "permissions": {
    "denyInsteadOfPrompting": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Two behaviors come with it that the release notes describe, and both matter for a run nobody watches.

First, the decision order does not change. Allow rules, the active permission mode, the auto-mode classifier, and your PermissionRequest hooks all decide before this setting kicks in. denyInsteadOfPrompting only replaces what happens to the calls that reach the prompt stage. It is a default for the leftover, not a bypass of the rules you already wrote.

Second, the release notes state that a streak of consecutive denials ends the turn. A single denied call is not the interesting case. The interesting case is a run that keeps hitting calls it is not allowed to make, and quietly stops after a few of them. That behavior is correct for your safety, and it is also the behavior that makes "the log is shorter than I expected" your only symptom.

One more line that pairs with it, for the non-interactive entry point: the same outcome is what --permission-prompts none produces when you call the -p form. If your job already launches with that flag, this setting is the version you get in a live terminal session and its subagents.

Background MCP tasks: the wait moved, it did not leave

The same release changes how a run spends its idle time. When an MCP server runs a tool call as a task, Claude Code now moves that call to the background immediately and reports the result when it finishes. The session no longer blocks on a slow query or a long build check. It keeps working, and the result arrives as a completion report.

That is a real unblocking. It is also a new place for a silent failure, for one reason: "it will report when it finishes" is a promise about a callback, and a callback that never fires leaves you with a task that is neither running nor reported. The notes ship the off-switch, CLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUND, which makes Claude Code wait for the task inside its tool call instead of backgrounding it. If your failure mode is a task that never reports, waiting inline at least gives you a hang you can time out, instead of a gap you cannot find.

This is the part I would test first, because it is the part most likely to look fine in the happy path and wrong in the one you ship.

The three places it can still go quiet

The setting removes one way of going quiet. These three remain, and none of them produce a red build.

  1. The turn ends after a streak of denials, and the end is not labeled as such. The run stopped because it kept being refused. The transcript shows a short turn, not a reason. If you only watch for errors, you see none.

  2. A backgrounded MCP task reports nothing. The session moved on, the task did not call back, and the result your plan depended on is simply absent. The run completed. The data did not.

  3. Behind a gateway, the classifier is not where you think it is. With auto mode, the safety check now runs on the server. In a session that cannot reach it (telemetry or feature flags off, or behind some LLM gateways), the notes say actions are asked about or denied with no fallback to the built-in classifier. CLAUDE_CODE_AUTO_MODE_SERVER controls that path, and the hosted Bedrock, Vertex, and Foundry routes are unchanged. A run that is "closed" because the server check could not run is closed for a different reason than a run that is closed because your policy said no.

The common thread is that each of these ends the run or drops a result without an error. "Failed closed" and "did less" are indistinguishable in a build log that only knows pass and fail.

The one script: make the denials countable

You cannot read the intent out of the run. You can count what it left behind. This script takes a stream-format transcript and reports three numbers, then returns a pass/fail on the one that matters for a parked run: are there still lines that look like a pending permission prompt?

#!/usr/bin/env bash
# audit_failclosed.sh <transcript.jsonl>
# Did the unattended run fail closed (denials you can count) or go quiet?
#   exit 0  no permission-wait lines remain (closed, or already finished)
#   exit 1  at least one line still looks like a pending permission prompt
#   exit 2  input missing or unreadable
set -uo pipefail
f="${1:-}"
[ -n "$f" ] && [ -r "$f" ] || { echo "usage: audit_failclosed.sh <transcript.jsonl>" >&2; exit 2; }

denied=$(grep -Eic 'denied|denyinsteadofprompting' "$f")
permwait=$(grep -Eic '(waiting|pending|blocked by).*(permission|approval)|(permission|approval).*(waiting|pending)' "$f")
bgdone=$(grep -Eic 'background.*(finish|complete|done|report)' "$f")

echo "denied tool calls       : $denied"
echo "permission-wait lines   : $permwait"
echo "background-task reports : $bgdone"

if [ "$permwait" -gt 0 ]; then
  echo "RESULT: OPEN  a permission prompt is still pending; the run can stall."
  exit 1
fi
echo "RESULT: CLOSED  no pending permission prompt in this transcript."
exit 0
Enter fullscreen mode Exit fullscreen mode

Be clear about what this is. It is a keyword-level read of the transcript, not a schema assertion. It will count a line that merely contains the word "denied" in prose. Treat the number as a signal to open the transcript, not as a verdict by itself. Its value is the shape of the check: you turn "is the run safe?" into "how many denials, and is anything still waiting," and you run that check in the same CI job that runs the agent.

Against three small fixtures (synthetic, shaped like the events a stream transcript carries, not real output from a live run):

$ ./audit_failclosed.sh closed.jsonl
denied tool calls       : 2
permission-wait lines   : 0
background-task reports : 1
RESULT: CLOSED  no pending permission prompt in this transcript.

$ ./audit_failclosed.sh open.jsonl
denied tool calls       : 0
permission-wait lines   : 1
background-task reports : 0
RESULT: OPEN  a permission prompt is still pending; the run can stall.
(exit 1)

$ ./audit_failclosed.sh missing.jsonl
usage: audit_failclosed.sh <transcript.jsonl>
(exit 2)
Enter fullscreen mode Exit fullscreen mode

The middle one is the one to wire into CI. exit 1 means the artifact still carries a prompt nobody will answer, and that is the case where a green pipeline and a parked run are the same log.

Where this stops, and how it differs from the hook path

This setting sits on the permission-prompt path. It is not the same mechanism as a tool guard that runs in front of a call and is expected to block with a specific exit code. On the hook path, the contract is about what the orchestrator does when your guard cannot reach a verdict. On the prompt path this post covers, there is no guard to crash. The tool asks, and the policy answers "no" and moves on.

That distinction matters when you combine the two. A run can have a guard that fails open on the hook path and still be closed on the prompt path, and the two will not show the same failure. If you run both, audit both separately: the guard by its exit-code contract, the prompt path by the denial count here.

The honest boundary of this whole approach: it makes the unattended run finish, and it makes the finish auditable. It does not make the policy right. A run that denies everything because the allow rules are too tight will pass this script's CLOSED and deliver nothing. The script tells you the run closed. Only you can tell you it closed for the right reason.

Sources

  • Claude Code v2.1.319 release notes, Anthropic (released 2026-11-06). Setting name, env-var names, and the background-task and auto-mode behaviors are as described in those notes.
  • World Programming, "Claude Code v2.1.319 unblocks unattended agent runs," 2026-11-06.
  • Havoptic Claude Code release tracker, entry for v2.1.319, 2026-11-06.
  • TurboAI, "Claude Code v2.1.319: background MCP tasks, auto-denied permission prompts," 2026-11-06. Source for the decision-order detail and the consecutive-denial behavior; the notes were relayed, so treat those two specifics as reported, not independently reproduced here.
  • Vibehacker, "Claude Code 2.1.319 backgrounds MCP tasks in the terminal," 2026-11-06.

The fixtures referenced by the script are synthetic and shaped like stream-transcript events. They are not output from a live run, and the counts they produce should not be read as measured Claude Code behavior.

This post was written with AI assistance. The author is responsible for its content.

Top comments (0)