CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-04Intermediate

Claude API stop_sequences Not Working — 5 Things to Check Before You Give Up

Diagnose why your Claude API stop_sequences parameter isn't halting generation as expected. Practical breakdown of token boundaries, whitespace mismatches, Tool Use interactions, and streaming pitfalls — with copy-paste code examples.

api38stop-sequencestroubleshooting87tokenizationstreaming21

"I passed \n\n to stop_sequences, and the output just keeps going" — if you've used the Claude API for any serious project, you've probably hit this. I burned half a day on the exact same issue while building a RAG-style format controller for one of the apps I maintain as an indie developer.

Here's the punchline: when stop_sequences looks broken, the real cause is almost always that the string Claude is generating doesn't byte-match the string you specified. This article walks through five concrete patterns I run into in production, each with diagnostic code you can paste in.

The stop_sequences contract you actually need to know

The stop_sequences parameter halts generation when a byte-exact match appears in the model's output. Anthropic's docs cover the basics ("up to 4 sequences", "stop_reason becomes stop_sequence"), but the gotchas live below that surface.

Three things to internalize before debugging:

  • The matcher only inspects what Claude is emitting right now, not your prompt
  • The matched stop string is stripped from the output (Tool Use is the exception)
  • stop_reason is one of end_turn, max_tokens, stop_sequence, or tool_use. Logging this single field makes debugging dramatically faster
# Always log stop_reason and stop_sequence
import anthropic
 
client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:"],
    messages=[{"role": "user", "content": "Count down from 5 to 1"}],
)
 
print(f"stop_reason: {response.stop_reason}")
print(f"stop_sequence: {response.stop_sequence}")  # The matched string, or None
print(f"content: {response.content[0].text}")

If stop_reason is end_turn or max_tokens, your stop string never appeared in the output at all. If it's stop_sequence but the cut-off point looks wrong, suspect whitespace or token-boundary issues — covered below.

Cause 1: Your stop string was never actually generated

This is by far the most common confusion. stop_sequences does not mean "don't let Claude output this string." It means "halt the moment Claude does output this string." If Claude never emits the marker, the parameter does nothing.

Example: you set stop_sequences=["```"] to cut off after a JSON code block, but Claude returns the JSON without code-fence markup. stop_reason will come back as end_turn.

The fix is to engineer the prompt so that the stop string is guaranteed to appear:

# ❌ Brittle: relies on Claude choosing to emit ```
prompt = "Return user info as JSON"
stop_sequences = ["```"]
 
# ✅ Robust: prompt explicitly requires the marker
prompt = """Return the user info in this exact format:
=== JSON START ===
{...JSON...}
=== JSON END ===
You must always emit === JSON END === after the JSON."""
stop_sequences = ["=== JSON END ==="]

The pattern that works in production: design a marker only your prompt would ever produce, then make emitting it a non-negotiable instruction. Markers like === END ===, <<<DONE>>>, or [/RESPONSE] are good choices because they are unlikely to appear naturally in user-generated prose, and they survive token-boundary issues better than punctuation-only markers like } or \n\n.

Cause 2: Whitespace and newline counts don't match

A frequent ticket: "I want to stop at \n\n, but it never fires." The cause is usually that Claude is outputting one \n, or three \ns — not exactly two. Because the matcher is byte-exact, off-by-one whitespace silently fails.

Diagnose by removing stop_sequences and inspecting the raw output with repr():

# Diagnostic step: remove stop_sequences and look at the actual bytes
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "Count down from 5 to 1"}],
)
print(repr(response.content[0].text))
# Example output: '5\n4\n3\n2\n1' — single \n, not double

If the actual output uses single newlines, switch to stop_sequences=["\n"], or add an explicit instruction to "leave a blank line between each item."

Cause 3: Stop sequences spanning token boundaries

This is the case that cost me the most time. Claude's tokenizer encodes strings like ### as one token in some contexts and as two or three in others.

Server-side matching is byte-based, so token boundaries shouldn't theoretically matter. But streaming responses are delivered token-by-token, and if your client is doing its own stop_sequences check on the SSE stream, a stop string split across tokens slips through your buffer.

There are two clean fixes:

# ✅ Trust the server's stop_sequences; don't re-check on the client
with client.messages.stream(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    stop_sequences=["=== END ==="],
    messages=[{"role": "user", "content": "..."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
 
    # Inspect the final assembled message for the authoritative stop_reason
    final = stream.get_final_message()
    print(f"\nstop_reason: {final.stop_reason}")
# ✅ If you must check client-side, buffer across token boundaries
buffer = ""
STOP = "=== END ==="
 
with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        buffer += text
        if STOP in buffer:
            final_text = buffer.split(STOP)[0]
            break

I lean toward option one whenever possible. Client-side double-checking is a fragile pattern that creates hard-to-reproduce edge cases.

Cause 4: Tool Use changes the rules

When you pass a tools parameter, the precedence order shifts. The moment Claude decides to call a tool, stop_reason becomes tool_use and the message ends — stop_sequences is only evaluated against text that comes before the tool call, not against tool inputs.

# stop_sequences interaction with Tool Use
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=2048,
    tools=[{"name": "get_weather", "input_schema": {...}, "description": "Get the weather"}],
    stop_sequences=["=== END ==="],
    messages=[{"role": "user", "content": "What's the weather in Tokyo? Append === END === at the very end."}],
)
 
# If Claude calls the tool, stop_reason will be "tool_use" and the turn ends here.
# stop_sequences will not have been evaluated for this turn.
print(response.stop_reason)  # "tool_use"

In a multi-turn agent loop, stop_sequences only fires on the final plain-text turn. If your goal is "stop a tool call from happening," you want the tool_choice parameter, not stop_sequences.

In my experience, more than half of "the agent won't stop" complaints come from conflating these two roles. The mental model that helps: stop_sequences is text-level termination, while tool_choice is decision-level routing. They live in different layers of the conversation and rarely substitute for each other cleanly.

Cause 5: Extended thinking is enabled

When extended thinking is turned on, stop_sequences only applies to the final answer text, not to thinking blocks. This is documented but easy to miss. If you set stop_sequences=["}"] to capture a JSON answer, any } inside the thinking block is ignored.

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=4096,
    thinking={"type": "enabled", "budget_tokens": 2000},
    stop_sequences=["}"],  # Only matches in the final text block
    messages=[{"role": "user", "content": "Return the calculation as JSON"}],
)
 
# response.content is a list of [thinking_block, text_block]
# stop_sequences only matches against text_block
for block in response.content:
    print(block.type, "->", block.text[:50] if hasattr(block, "text") else block.thinking[:50])

When mixing extended thinking with stop sequences, design a marker that is guaranteed to appear in the final answer section, not in the reasoning trace.

A diagnostic checklist you can run top-to-bottom

The next time stop_sequences misbehaves in production, walk this list:

  • Check stop_reason first (end_turn → Cause 1; stop_sequence → narrow further; tool_use → Cause 4)
  • Remove stop_sequences and inspect raw output with repr() to confirm the bytes
  • Verify newline and whitespace counts match exactly
  • For streaming, check whether you're double-matching on the client
  • Confirm whether tools is passed and how it interacts with this turn
  • Confirm whether extended thinking is enabled

If none of those isolate the issue, the most leveraged fix is usually changing the design so the stop marker is something Claude is forced to emit by your prompt.

A small but valuable habit: in any production code that depends on stop_sequences, return the matched stop string back to your caller along with the text. When something downstream breaks weeks later, having the actual stop string in the logs (rather than just "it stopped") tells you immediately whether the model halted on the marker you intended or on something else entirely.

When to step back from stop_sequences entirely

A short editorial to close. stop_sequences is fundamentally string-matching control, and string matching sits in tension with the flexibility of natural-language generation. As an indie developer running several apps alongside the Dolice Labs sites, I've spent the past six months leaning away from stop_sequences and toward two alternatives whenever I need format guarantees:

  • Tool Use (function calling): Define a JSON-schema tool and receive the structured output as tool_use content rather than freeform text
  • Constrained sampling APIs: Where the SDK exposes response_format or similar primitives, prefer them over post-hoc stop matching

Before sinking another afternoon into a stop_sequences bug, it's worth asking whether stop sequences are even the right tool for what you're trying to enforce.

For deeper understanding of the API surface, the Claude API Token Counting and Cost Optimization Guide and Claude API Streaming Tool Use Implementation are good companions to this article.

The single highest-leverage move you can make right now: open one piece of code that uses stop_sequences and add three lines to log stop_reason and stop_sequence next to the response. The next time this fails in production, you'll have your answer in a minute instead of an afternoon.

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

API & SDK2026-04-26
Claude API Streaming Stops Mid-Response: Diagnosing and Fixing the 5 Root Causes
When Claude API streaming stops unexpectedly, there are exactly 5 root causes. Learn to diagnose which one you're hitting and apply the right fix — from timeout tuning to stop_reason logging.
API & SDK2026-06-27
When Claude API Streaming Stops Without an Error: Detecting Silent Stalls and Resuming Mid-Stream
How to catch the 'silent stall' where Claude API streaming stops with no exception at all, using a content-level watchdog that times the gap between tokens, plus a resume path that carries received text forward as an assistant prefill, and a four-layer timeout budget for long-running automation.
API & SDK2026-05-28
Why JSON.parse Fails on Claude API Streaming tool_use Arguments — and How to Fix It
When you stream a Claude API response with tool_use, calling JSON.parse on each input_json_delta throws SyntaxError. Here is the correct way to assemble partial_json fragments, plus disconnect handling.
📚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 →