Claude Code permissions.denyInsteadOfPrompting: the overnight session stops waiting, and refused work raises no error
The new setting removes the wait, not the failure. A refused call ends in a clean exit and a success subtype, so the setting is only half the setup: the other half is a record of what was refused. The same release gave it more to refuse, by taking away auto mode's fallback for sessions that don't fetch feature flags.
Contents
A Claude Code session left alone overnight can stop on a permission prompt that nobody sees until morning. The existing ways around that each cost something. bypassPermissions asks about nothing. dontAsk refuses whatever would prompt, but it is a permission mode, so it replaces auto mode. --permission-prompts none does the same for a single claude -p run. Claude Code 2.1.319, released on November 6, adds permissions.: a setting that refuses whatever would wait on a person, in any kind of session, underneath whichever mode you run.
The same release gave that setting more to refuse. With telemetry or feature flags off, auto mode calls that the server can’t check no longer fall back to Claude Code’s own classifier requests; they are asked about, which with the setting on means refused. I ran the tests below on Claude Code 2.1.322 on Linux on November 11, with Haiku 5.5 in a throwaway repository. Where a passage rests on the release notes or the docs alone, it says so.
What does Claude Code permissions.denyInsteadOfPrompting do?
It turns a permission prompt into a refusal. Allow rules, your permission mode and the auto mode classifier still decide first; only a call that would otherwise stop and wait for a person is refused, and Claude is told to carry on with the parts of the task that need no approval. The settings reference lists it for -p runs, interactive terminal sessions, the VS Code extension, local Desktop sessions, subagents and agent teams.
Four more details come from the settings reference; I did not test them:
- Where it can live. User or managed settings, or
--settings. A repository’s.claude/settings.jsoncan’t set it for you, and atruein~/.claude/settings.jsonor--settingswins even over a managedfalse. - A setting, not a mode. It sits under auto mode, so the classifier keeps approving routine calls and the setting only catches what would fall through to a prompt.
- What else it switches off.
AskUserQuestion,EnterPlanModeandExitPlanModeleave the tool list, so a session in plan mode can’t leave it on its own (teammates in an agent team keep the two plan tools). MCP elicitation requests are cancelled unless anElicitationhook answers them. AcanUseToolcallback or--permission-prompt-toolis no longer asked. Where the notice about billed classifier requests would open a dialog, the notice is skipped and the actions that need those requests are refused. - When it stops anyway. In auto mode with the setting on, the turn ends after 20 refused calls in a row with no work in between (“20 actions were refused and no work went ahead in between, so Claude stopped.”), and a
-prun reports that aserror_during_execution. “Carry on” is per call, not a promise that the night’s turn keeps going.
Can a Claude Code session with denyInsteadOfPrompting still stall?
Yes, on the two things the setting doesn’t cover: the workspace trust dialog and the approval of project MCP servers from .mcp.json. The docs say so, and my first interactive session stopped on exactly that. I started it with the setting on in a directory Claude Code had not seen before, and it sat on “Quick safety check: Is this a project you created or one you trust?” until I answered.
For an interactive or background session, that makes one step part of the setup: start Claude Code once by hand in the directory the job will use, accept the trust dialog and any project MCP servers, then quit. Per the permissions docs, trust is keyed on the repository root, and a worktree uses the main checkout’s root, so a new worktree of a trusted repository doesn’t ask again; a fresh clone in a new place does. A claude -p run shows neither dialog: it connects project MCP servers without asking and simply skips the project’s allow rules until the folder is trusted. In a tmux pane the trust dialog is worse, because its default option is No, exit; the tmux post covers what a stray Enter does to Claude Code dialogs.
How do you set up Claude Code denyInsteadOfPrompting for an overnight session?
Use 2.1.319 or later for the setting; 2.1.321 also fixed three permission checks that matter when nobody is watching (a read-only Bash command run outside the sandbox without a prompt, a write judged from the wrong folder, deny rules missing the file a cp or mv into a directory writes), so I would not run this recipe below that. The docs’ own example puts the setting in user settings next to the start mode:
{
"permissions": {
"defaultMode": "auto",
"denyInsteadOfPrompting": true
}
}That file applies to every session you start, including the ones you sit in front of. To scope it to the unattended job, pass it at launch:
claude --permission-mode auto \
--settings '{"permissions":{"denyInsteadOfPrompting":true}}'For a single claude -p run there is the older flag, --permission-prompts none (v2.1.259 and later). Per the headless docs it matters most when an Agent SDK canUseTool callback or a --permission-prompt-tool would otherwise be asked and waited on; a -p run with no such host already refuses what would prompt, which my control run below confirms. The setting is what also covers interactive and background sessions.
The docs’ reasoning implies the setting changes little in bypassPermissions, where almost nothing prompts; I did not run that combination. It earns its place in auto mode or under narrow allow rules, where some calls still fall through to a prompt.
What does a refused call look like in Claude Code with denyInsteadOfPrompting?
In an interactive session there is no dialog at all. I started Claude Code in a tmux pane in manual mode with the setting passed through --settings and asked it to create a file. The call showed “Not run · permission denied”, and Claude received this as the tool result (path shortened):
Permission for this tool use was denied. It requires approval, and this session has no approval surface — nobody can answer a permission prompt here — so it was denied automatically. The action was NOT performed; do not claim it succeeded, and do not retry it: this action, and anything else that requires approval, will be denied the same way for the rest of this session. Tell the user what was blocked and why you needed it, then continue with the parts of the task that do not require approval. What required approval: Claude requested permissions to write to /tmp/dipt/hello.txt, but you haven't granted it yet.Claude half did what that text asked. Its first attempt was a shell redirect; after that was refused, it tried the Write tool anyway, and that was refused too. Only then did it report “Not done: I couldn’t create hello.txt”, name both attempts and suggest a ! command to run by hand. Haiku took the same second route in one of the two -p runs with the setting. A second interactive session, asked to run date > now.txt, stopped after the first refusal and said it had not tried another way “since that would get around the denial”. Same model, same instruction, two behaviors: “do not retry” is text the model reads, not a lock.
In claude -p the setting is harder to see, because a -p run without a permission host refuses prompts anyway. I ran the same task with and without it. Both runs exited 0 with "subtype": "success" and "is_error": false, both listed the refused call in permission_denials, and both emitted a permission_denied message in stream-json. What differed was the message: without the setting, Claude got only “Claude requested permissions to write to … but you haven’t granted it yet”, and the permission_denied event carried no reason; with it, Claude got the full do-not-retry text above, and the event carried a reason:
"decision_reason": "no approval surface in this session; permission request denied automatically"How do you catch work that Claude Code skipped under denyInsteadOfPrompting?
Not by the exit code. Every refused call in my -p runs ended in exit 0 and a success subtype, and the interactive sessions simply went back to idle. Haiku’s summaries were candid each time, but a summary is the model’s account of its own work, and an overnight job should not depend on someone reading prose. Two mechanical checks work.
For -p runs, keep Claude’s own exit code and fail on anything else that is off, including refused calls:
claude -p "$TASK" --permission-mode auto --output-format json \
--settings '{"permissions":{"denyInsteadOfPrompting":true}}' > run.json
rc=$?
python3 - "$rc" <<'EOF' || exit 1
import json, sys
rc = int(sys.argv[1])
try:
d = json.load(open("run.json"))
except Exception:
sys.exit("no readable result")
n = len(d.get("permission_denials", []))
print(f"exit={rc} subtype={d.get('subtype')} is_error={d.get('is_error')} refused={n}")
sys.exit(1 if rc or d.get("is_error") or d.get("subtype") != "success" or n else 0)
EOFThat tells you the run hit no refusal and no error. It doesn’t tell you the task is done; that check belongs to the task.
For interactive and background sessions there is no result object, so record the request when it happens. The obvious hook, PermissionDenied, does not fire here: per the hooks reference it runs only when auto mode refuses a call, not for a deny rule or a prompt nobody can answer. PermissionRequest runs before Claude Code would show the prompt, so it sees the call before the setting refuses it. A hook that writes the request down and returns no decision leaves the refusal in place. Save it as ~/. and chmod +x it:
#!/usr/bin/env python3
# PermissionRequest hook: record the call, decide nothing.
import json, os, sys, datetime
e = json.load(sys.stdin)
row = {
"t": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
"session": e.get("session_id"),
"tool": e.get("tool_name"),
"input": e.get("tool_input"),
}
with open(os.path.expanduser("~/.claude/prompt-requests.log"), "a") as f:
f.write(json.dumps(row) + "\n")Load it in the same --settings file as the setting, so it runs only for the unattended job and not for the prompts you approve by hand during the day:
{
"permissions": { "denyInsteadOfPrompting": true },
"hooks": {
"PermissionRequest": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "~/.claude/hooks/log-request.py" }] }
]
}
}I loaded it that way in a -p run and in an interactive tmux session. It recorded the refused Write in the first and the refused Bash call in the second, and both calls stayed refused. The second row, session id shortened:
{"t": "2026-11-11T02:25:43+00:00", "session": "7a533308-…", "tool": "Bash", "input": {"command": "date > now.txt", "description": "Write current date to now.txt"}}The log has three limits. A row is a request that would have prompted; it is a refusal only while the setting is on and no other PermissionRequest hook approves the call, so if you also run approving hooks, they decide what a row means. Filter by session, not by time, since the file is shared across sessions. And an empty log proves nothing unless the hook ran: pipe a sample event into the script once, and make one call you know will be refused, before you trust an empty file. Per the settings reference, some calls are refused before any hook is asked (a critical-path removal in auto mode that no ask rule covers), so the log is a floor, not a full list. For auto mode refusals, a PermissionDenied hook covers the rest. The hooks post covers why a hook is better at steering than at guarding.
What changed in Claude Code auto mode with telemetry or feature flags off?
A session-level fallback went away. Per the permission modes docs, since 2.1.282 a session that doesn’t fetch feature flags asks the server to review auto mode actions by default. When the server doesn’t review the session at all (a gateway that drops the review fields, a platform or credential without server checks), Claude Code used to fall back to its own classifier requests for the rest of the session. The 2.1.319 release note changes that for sessions “with telemetry or feature flags off, including behind LLM gateways”: calls the server can’t check “are asked about or denied, with no fallback to the built-in classifier”. With denyInsteadOfPrompting on, “asked about” becomes “denied”. A single action that gets no verdict from a reviewing server was already refused before this release; that part is not new.
Who it hits depends on the connection more than on the telemetry switch. In my session on a direct connection to the Anthropic API, turning telemetry off did not turn server review off: with DISABLE_TELEMETRY=1, /status in auto mode read Auto mode server: Enabled, and a -p run in auto mode ran a shell command without a refusal. The case the change was written for is an LLM gateway or proxy that drops the review request or its results. Those sessions used to get Claude Code’s own classifier requests and now get prompts or refusals. None of my tests ran behind a gateway, so that part rests on the release note; the gateway post covers what a gateway has to pass through untouched. Bedrock, Vertex and Foundry are unchanged, per the same note.
Two lines of that note are easy to misread. CLAUDE_CODE_DISABLE_FEATURE_FLAGS, also new in 2.1.319, stops flag fetching while leaving telemetry on, and it does not restore the fallback: the note names “telemetry or feature flags off” together, so switching from DISABLE_TELEMETRY changes what you send, not how auto mode behaves. CLAUDE_CODE_AUTO_MODE_SERVER= does restore it, by not asking the server at all. The docs note that on Enterprise plans and on API, Bedrock, Vertex and Foundry accounts those classifier requests count toward token usage, which server review does not add.
The permission modes page, as I read it on November 11, still describes the session-level fallback without the 2.1.319 exception. For a session with telemetry or flags off, go by the release note until the docs catch up.
How do you check which path Claude Code auto mode is using?
Run /status and read the Auto mode server line. In my sessions it showed Enabled with DISABLE_TELEMETRY=1 alone and Disabled once CLAUDE_CODE_AUTO_MODE_SERVER= was added. 2.1.319 also fixed the line showing Enabled when the review wasn’t reaching the session, for example behind a gateway that drops it; on earlier versions an Enabled there proved nothing. Once you know the server is not reviewing, the auto mode setup post shows how to check which model Claude Code’s own classifier runs on.
The same release fixed a -p exit code: when auto mode refused actions because no safety check was available, claude -p used to exit 0 and now exits 1 in text and JSON output. That fix covers auto mode with no check, not refusals in general; my default-mode refusals still exited 0, which is why the script above counts permission_denials as well. Per the hooks reference, a PermissionDenied hook sees a no-verdict refusal with a reason that starts “Auto mode could not evaluate this action and is blocking it for safety”, which tells a missing check apart from a classifier that looked at the call and said no.
Should you turn on Claude Code denyInsteadOfPrompting?
For an interactive or background session nobody watches, yes, and only together with the log. There the setting trades a failure you can see for one you have to look up. A stalled session is obvious in the morning: the pane is sitting on a dialog and nothing else has happened. A session with the setting finishes its turn, and the refused call is in the summary, in permission_denials for a -p run, and in the hook log if you set one up. Nothing fails on its own. For claude -p the setting changes less: those runs already refused, and what it adds is the do-not-retry text, which one of my runs ignored. The docs say Claude carries on with the steps that need no approval; my tasks were one step each, so all they show is the refusal.
The auto mode change pushes the same way. Behind a gateway that drops server review, 2.1.319 turns a quiet fallback into prompts, and with the setting on, into more refused calls. Without the log, a stalled session is the more honest failure.
01 / Discussion
No comment section here — all discussions happen on X.
02 / More articles
Claude Code slash commands: the built-ins worth knowing, and why your custom /command is a skill now
Treat the commands folder as code Claude can call, not as macros. Audit which files leave model invocation on, check the turn count in scripted runs instead of trusting the exit code, and put the guarantees for anything with side effects in permissions and hooks rather than in an invocation flag.
Claude Code vs Gemini CLI: seven ways developers split the work, and which ones survived June 18
The 2025 setups that paired the two tools leaned on a free Google login and a bigger context window. The login went to another tool in June, and Claude Code caught up on the window. What decided which setups survived was the account behind each call, not the tools themselves.
Claude Code background subagents and your usage limit: Anthropic's 99% status-line claim, and the cache writes you can measure
Isolation is the point of a subagent, and in my transcripts most of its bill was five-minute cache writes. Measured on a Max plan: background subagents about 11.5% of priced usage, 64% of that in cache writes. A Python script to reproduce it, and four levers with the part of the bill each one acts on.