CLAUDE LABJP
FABLE 5 — Claude Fable 5 is available again to users worldwide from July 1 after US export controls were liftedSCIENCE — Claude Science, a workbench for researchers, is in beta; the AI for Science credit program is open through July 15CODE — Claude Code adds dynamic workflows (research preview) and raises weekly usage limits by 50% through July 13MODEL — Claude Sonnet 5 is the default across all plans at $2/$10 per million tokens through August 31GATEWAY — A self-hosted Claude apps gateway arrives for Amazon Bedrock and Google Cloud (SSO, policy, cost control)SECURITY — A new cybersecurity classifier ships alongside the Fable 5 redeploymentFABLE 5 — Claude Fable 5 is available again to users worldwide from July 1 after US export controls were liftedSCIENCE — Claude Science, a workbench for researchers, is in beta; the AI for Science credit program is open through July 15CODE — Claude Code adds dynamic workflows (research preview) and raises weekly usage limits by 50% through July 13MODEL — Claude Sonnet 5 is the default across all plans at $2/$10 per million tokens through August 31GATEWAY — A self-hosted Claude apps gateway arrives for Amazon Bedrock and Google Cloud (SSO, policy, cost control)SECURITY — A new cybersecurity classifier ships alongside the Fable 5 redeployment
Articles/Cowork
Cowork/2026-07-05Intermediate

When Claude Declines a Request on Safety Grounds, What Should an Unattended Pipeline Return?

A third kind of ending that is neither an error nor a normal completion — a safety decline. Here is how to fold it into a pipeline you run unattended, with a classifier and a review-queue design drawn from indie development.

cowork11automation86unattendedreliability14

Reviewing the logs of a nightly job the next morning, I found one run whose ending looked like neither the usual error nor the usual artifact. No exception was raised. The HTTP status was 200. And yet the body held not the work I had asked for, but a polite refusal: "I can't help with that request." The pipeline read a returned response as success and wrote the refusal itself out as the deliverable.

Not an error, and not the completion I wanted. This safety-grounded decline is the third ending that unattended pipelines most easily drop. What a human beside the machine would spot at a glance slips silently past automation that only ever asks: success or failure?

In July 2026, Claude Fable 5 resumed worldwide availability, and with it came a new cybersecurity classifier meant to curb misuse of the top-tier model. Now that individuals routinely reach for frontier models, it has become genuinely more likely that legitimate work occasionally brushes against a safety judgment. That is exactly why it is worth treating a decline not as an exception, but as one of the normal branches your design should anticipate.

Two branches are not enough

Most pipelines treat the ending of a single call as a binary — success or failure. In reality you need to distinguish at least four states.

EndingTypical signsWhat to do
Normal completionHTTP 200 / stop_reason is end_turn / body has the expected shapeAccept as the deliverable
Infrastructure failureHTTP 429, 529, 5xx / timeoutRetry with exponential backoff
Safety declineHTTP 200 but the body is a refusal unrelated to the ask / stop_reason is refusalDo not retry; move to a human review queue
Degraded / emptyHTTP 200 but the body is empty or cut off mid-wayFail loudly via a done-condition assertion

Of these four, the infrastructure and degraded cases were covered in Reading Claude API stop_reason Correctly — A Production Guide to end_turn, max_tokens, pause_turn, and refusal and Logged as success, but it produced nothing — stopping silent failures in Cowork scheduled tasks with end-of-run assertions. What I want to go one level deeper on here is the third one: the safety decline.

Don't conflate a decline with an infrastructure error

The awkward thing about a decline is that at the HTTP layer it looks like success. As long as you watch only the status code, a decline hides inside a "normal 200." To tell them apart you have to read three things together: the status, the stop_reason, and the body.

from dataclasses import dataclass
from enum import Enum
 
 
class Outcome(Enum):
    OK = "ok"                    # a normal completion you may accept
    INFRA_ERROR = "infra_error"  # a transient failure you may retry
    DECLINED = "declined"        # declined on safety grounds
    DEGRADED = "degraded"        # empty or truncated
 
 
@dataclass
class RunResult:
    http_status: int | None
    stop_reason: str | None
    text: str
 
 
def classify(result: RunResult) -> Outcome:
    # 1. Infrastructure first (failure at the network layer)
    if result.http_status is None or result.http_status >= 429:
        return Outcome.INFRA_ERROR
 
    # 2. The model returned an explicit refusal
    if result.stop_reason == "refusal":
        return Outcome.DECLINED
 
    # 3. 200 but the body is empty / extremely short = degraded
    if not result.text or len(result.text.strip()) < 40:
        return Outcome.DEGRADED
 
    # 4. Everything else is a normal completion
    return Outcome.OK

The key move is to catch refusal first and never drop it into the same bucket as an infrastructure error. Mix the two, and a machine will retry, over and over, something that actually needs human judgment. Retry is the remedy for a transient hiccup; it does nothing for a decline. Resend the same request under the same conditions and you get the same refusal back.

A decline that is not surfaced as an explicit stop_reason == refusal — a polite demurral inside the body — does happen in practice. If you want to catch it reliably while running unattended, verify a structural done-condition the deliverable must satisfy (for example, "contains at least two headings" or "has the required JSON keys") in addition to stop_reason, and hold any 200 that fails it as DEGRADED. Rather than having a machine adjudicate whether a body "means" a refusal, you stop on the observable fact that it "does not have the expected shape." That is a deliberate simplification, and a safe one.

The anti-pattern: auto-rewording a declined request to push it through

This is the single point I most want to land. When you receive a decline, it is tempting to wire in a remedy that mechanically rewords the prompt and resends until it goes through. Don't.

There are two reasons. First, if the decline was well-founded, engineering around it with automation runs against the intent of the safety design itself. What should be declined should stay declined. Second, even if it was a false positive on legitimate work, only a person who knows the context of that work can judge whether a rewording is warranted. Hand an unattended loop the job of "getting it through," and the most important step — confirming legitimacy — is exactly the one that goes missing.

Legitimate work does occasionally brush a judgment. The right response in that case is not to have a machine reword it, but for a person to re-issue the request with the concrete purpose and background attached. Return the decision to a human. The job of an unattended pipeline is not to push through, but to stop and hand off.

Move declines to a review queue

So how do you hand off after stopping? On a decline, move that run's input and output — in full — to a place a person can review later. Not accepted as a deliverable, not counted as success, but not lost either. This holding place is the review queue.

import json
import hashlib
from datetime import datetime, timezone
from pathlib import Path
 
REVIEW_DIR = Path("review_queue")
 
 
def enqueue_for_review(task_id: str, prompt: str, result: RunResult) -> Path:
    REVIEW_DIR.mkdir(exist_ok=True)
    # An idempotency key that folds repeated declines of the same input into one
    key = hashlib.sha256(f"{task_id}\n{prompt}".encode()).hexdigest()[:16]
    path = REVIEW_DIR / f"{key}.json"
 
    record = {
        "task_id": task_id,
        "queued_at": datetime.now(timezone.utc).isoformat(),
        "outcome": "declined",
        "prompt": prompt,
        "response_text": result.text,
        "stop_reason": result.stop_reason,
    }
    path.write_text(json.dumps(record, ensure_ascii=False, indent=2))
    return path
 
 
def handle(task_id: str, prompt: str, result: RunResult) -> str:
    outcome = classify(result)
    if outcome is Outcome.DECLINED:
        enqueue_for_review(task_id, prompt, result)
        return "held"          # neither success nor failure, but "held"
    if outcome is Outcome.INFRA_ERROR:
        raise TransientError(result.http_status)  # send to retry
    if outcome is Outcome.DEGRADED:
        raise DefinitionOfDoneError(task_id)      # fail on done-condition
    return "ok"
 
 
class TransientError(Exception): ...
class DefinitionOfDoneError(Exception): ...

Folding repeated declines of the same input into one record with an idempotency key keeps the queue from bloating night after night with identical entries. Review the queue once a week and you begin to see patterns in what gets declined. If a particular topic keeps brushing the judgment, that is a signal to handle that area by hand rather than unattended.

Another benefit of this design is that giving the pipeline a third return value — "held" — makes its logs honest. Don't count declines as successes, and your success rate reflects reality; don't count them as failures, and you stop burning retries and alerts on them needlessly. The instinct to give a long unattended run a ceiling and a place to set things aside is continuous with Putting a Ceiling on the pause_turn Loop: Running Long Server Tools Safely Unattended.

Wrapping up, and a next step

A safety decline is not a fault for an unattended pipeline; it is one of the normal branches to plan for. Add a third state — "held" — to your two-way success/failure check, classify declines apart from infrastructure errors, never auto-reword to push through, and hand off to a review queue that returns the decision to a person. Wire in just these four things, and an ending that used to slip by silently will be plainly in front of you the next morning.

Start by picking a single scheduled job that matters most to you, and slip a classify-style check into one call. If it catches even one decline as "held," the mechanism is worth spreading to your other tasks. Running several sites unattended as an indie developer under Dolice, I found that once I had this holding place in place, I could hand the nightly work over and rest easier. Thank you for reading.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Cowork2026-06-27
Logged as success, but it produced nothing — stopping silent failures in Cowork scheduled tasks with end-of-run assertions
A Cowork scheduled task exits 0, yet not a single artifact was produced. Trusting the exit code alone hides this silent failure. Here is how to turn your definition of done into end-of-run assertions that fail loudly with an evidence log.
Cowork2026-06-30
Trusting a Three-Day-Old Mirror: Stopping Unattended Tasks from Acting on a Stale Working Copy
A persistent clone reused for speed quietly drifts from the remote. Read 'has this article been fixed yet?' from an out-of-sync tree and your unattended task duplicates work or 'succeeds' against an old world. Here is a HEAD-match plus writability preflight, with a self-healing re-clone, in bash.
Cowork2026-05-04
Why Cowork Scheduled Tasks Stop Mid-Run and How to Recover
A systematic guide to diagnosing and recovering from Cowork scheduled task failures — covering permission dialog lockups, bash-only file operations, disk space exhaustion, and git lock conflicts.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →