CLAUDE LABJP
TEACHERS — Anthropic launches Claude for Teachers, giving verified US K-12 educators free premium access, teaching skills, and curriculum links aligned to standards in all 50 statesADMIN — The Admin API is now in beta for every Claude Enterprise organization: member and invite calls need no beta header, while group and custom-role calls doM365 — The Microsoft 365 connector gains write tools — draft, send, and organize email, manage calendar events and mailbox settings, and create or update files in OneDrive and SharePointMCP — Fixed servers from --mcp-config or .mcp.json ignoring a per-server request_timeout_ms, which left long-running tool calls timing out at the 60s default in fresh sessionsSUBAGENT — A new --forward-subagent-text flag and CLAUDE_CODE_FORWARD_SUBAGENT_TEXT variable include subagent text and thinking in stream-json outputDEADLINE — Opus 4.7 fast mode is removed on July 24, seven days out. speed: "fast" will error, so confirm your move to Opus 4.8TEACHERS — Anthropic launches Claude for Teachers, giving verified US K-12 educators free premium access, teaching skills, and curriculum links aligned to standards in all 50 statesADMIN — The Admin API is now in beta for every Claude Enterprise organization: member and invite calls need no beta header, while group and custom-role calls doM365 — The Microsoft 365 connector gains write tools — draft, send, and organize email, manage calendar events and mailbox settings, and create or update files in OneDrive and SharePointMCP — Fixed servers from --mcp-config or .mcp.json ignoring a per-server request_timeout_ms, which left long-running tool calls timing out at the 60s default in fresh sessionsSUBAGENT — A new --forward-subagent-text flag and CLAUDE_CODE_FORWARD_SUBAGENT_TEXT variable include subagent text and thinking in stream-json outputDEADLINE — Opus 4.7 fast mode is removed on July 24, seven days out. speed: "fast" will error, so confirm your move to Opus 4.8
Articles/Claude Code
Claude Code/2026-07-17Advanced

The String I Approved Wasn't the String I Read — Testing a Relayed Permission Prompt with Deceptive Characters

I pushed bidi overrides and zero-width characters through my own approval relay. NFKC normalization caught 0%. Here is why, and the implementation that catches 100% with zero false positives.

Claude Code195Permissions2Security8UnicodeAutomation38

Premium Article

Once I moved my nightly updates to unattended runs, the only thing left in my hands was the approval.

The four sites I run at Dolice Labs go from article generation through quality gates to push without me. When something risky comes up, a permission prompt lands in my chat, and I read it and let it through. More than once I've approved a single line while standing on a train platform.

A recent Claude Code update carried a short note: permission previews relayed to chat channels now neutralize bidirectional override characters, zero-width characters, and lookalike quotation marks. The point of the fix is that tool input shouldn't be able to change how an approval message looks.

That stopped me.

If the look can be changed, then what I had been approving all along was never the string that runs. It was the appearance of the string that runs. Those two things usually match. I had no particular reason to believe they always would.

So I pushed deceptive strings through my own relay, and the results weren't what I expected.

Where the deception comes from

The gap between how a string looks and what it contains exists because Unicode carries characters that are instructions for display. The layer that renders obeys them. The layer that executes ignores them and reads bytes. That asymmetry is the whole opening.

TrickCode pointWhat happens
Bidi override (RLO)U+202EEverything after it renders right-to-left, so the tail appears reversed
Zero-width spaceU+200BOccupies no width and hides inside a word. Your eyes miss it; so does your regex
Zero-width non-joinerU+200CSame idea. Slip one inside a flag name and it becomes a different flag
Lookalike quotesU+201C / U+201DLooks quoted. To the shell, it's just a character
HomoglyphU+043F, etc.Cyrillic п passing as Latin n. A different identifier entirely
NewlineU+000ALets an attacker add fake lines to the preview — including one that says "approved"

That last row is the one that got me. The goal of the deception isn't only to make a dangerous command look safe. It also works by making the person reading believe someone already checked. The target isn't the terminal. It's me, reading the screen.

The upstream fix protects the upstream screen

It's worth being precise about where the trust boundary sits.

The fix on the Claude Code side sanitizes the permission preview that Claude Code assembles. That's the right fix, and it holds as long as you use the default path.

My setup steps outside it. I consume unattended runs as stream-json, format the requests that need confirmation with my own bot, and relay them to chat. The formatting is my code. So a string that was sanitized upstream gets handed back to me and re-rendered by my renderer. The upstream fix cannot reach inside my renderer.

SegmentWho protects itState in my setup
Tool input → Claude Code previewClaude Code (fixed)✅ Protected
stream-json → my bot's formattingMe⚠️ Passed through raw
Bot → chat renderingThe chat client⚠️ Left to the client
Approval → actual executionMe⚠️ Never verified

Prompt injection defense — scrubbing what you feed the model — is a well-worn topic. That protects the model. What's at stake here runs the other direction: the channel from the model to the human. In my setup, nobody owned the job of protecting the approver.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Measured results across four layers: plain relay 0%, control-char stripping 50%, NFKC normalization 0%, escaped rendering 100%
A naive non-ASCII rule blocks 40% of my real commit messages. A script allowlist gets 0% false positives at 100% detection
A working approval-token implementation that separates what you display from what you verify
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

Claude Code2026-07-05
Rolling Out Trusted Devices for a Small Team: Enrollment, Preflight, and Rotation
How to introduce Team/Enterprise Trusted Devices for a 2-5 person team: device enrollment, an unattended-run preflight gate, and closing the gaps that appear during device rotation and offboarding.
Claude Code2026-06-29
Borrowing the Trusted Devices Idea for a Solo Setup: Pinning Automated Runs to Devices You Approved
The Trusted Devices feature that landed in Claude Code on June 28, 2026 is for Team and Enterprise. You can't use the feature on a solo plan, but you can borrow the idea. Here is how to identify a device in a way that doesn't break, and stop any automated run that starts from a device you never approved — with working code and the traps to avoid.
Claude Code2026-06-28
Stop Leaving a Static API Key in Your Unattended Jobs — Move to Short-Lived WIF Credentials
Claude Code is moving from static API keys toward short-lived, scoped credentials via WIF (Workload Identity Federation). Here is how to translate that idea to a small unattended pipeline so a leaked key has a much smaller blast radius — with working code and the failure modes to watch.
📚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 →