CLAUDE LABJP
RELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommandPERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to askMEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display windowMCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no outputRUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connectionLIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four daysRELEASE — Claude Code v2.1.246 shipped on August 26, adding a startup warning for Bash allow rules that put a wildcard ahead of the subcommandPERMISSIONS — /permissions now has an Auto mode tab, so you can see in one place what runs automatically and where Claude still stops to askMEMORY — Unbounded memory growth in long interactive sessions is fixed: subagent tool results are released once they scroll out of the recent display windowMCP — In headless and remote sessions, a tool call interrupted by an incoming message is now reported as an explicit interrupted error instead of completing with no outputRUNNER — claude self-hosted-runner gains --proxy-authorization-command and --proxy-authorization-file for egress proxies that issue a fresh auth header on every connectionLIMITS — The 50% weekly limit increase runs through August 31 for Pro, Max, Team, and seat-billed Enterprise accounts, which leaves four days
Articles/Claude Code
Claude Code/2026-08-28Beginner

A small script that catches invisible characters in plugin names before you install

Control characters and zero-width characters hiding in plugin or skill names are invisible in a terminal listing. I built six deliberately confusing names, compared what ls actually shows, and wrote a short check you can run before installing anything — plus an honest note on what that check misses.

claude-code131plugin3security18unicodemarketplace

I stopped trusting my own eyes the moment I put six near-identical directory names into a single ls listing. Exactly one of those six rows was really claude-helper. The other five looked the same width, roughly the same length, and carried completely different bytes.

What sent me down this path was Claude Code v2.1.247, released on August 26. Buried in the security section of the Claude Code changelog is a line saying that marketplace names containing control or invisible characters are now rejected. It reads like a small housekeeping item. But when I asked myself how I would verify the state of my own machine, I had no answer. So I sat down and worked it out.

As an indie developer I install plugins and skills alone, use them alone, and review them alone. The fewer colleagues there are to catch something, the more it pays to own one check that does not depend on eyesight.

Six names that a listing could not tell apart

I built six deliberately confusing names. All of them are mine, created for this test — none were collected from the wild.

VariantCharacter mixed inCharactersUTF-8 bytes
Plain namenone1313
Zero-width spaceU+200B1315
Zero-width joinerU+200D1416
Right-to-left overrideU+202E / U+202C1822
Cyrillic aU+04301314
Soft hyphenU+00AD1314

The row worth pausing on is the zero-width space. It has exactly the same character count as the plain name — thirteen. Counting characters buys you nothing. The only difference is the byte count: 13 against 15.

Here is what ls produced. Row order varies by environment, but this is the entire signal available to your eyes:

claude-helper
claude-hel[U+200D]per
claude-[U+202E]resu-daer[U+202C]
claude[U+00AD]helper
claude[U+200B]helper
clаude-helper

The bracketed labels are markers I added for this article. Your terminal prints nothing at all in those positions, so on a real screen you get six rows stacked in nearly identical shapes. That I had to annotate them just to make them visible in a blog post is, I think, a fair summary of the whole problem.

Recovering the original names by staring at that output is not realistic. I could not do it. Pipe the same listing through cat -v, though, and the picture changes completely.

$ ls -A d | cat -v
claude-helper
claude-helM-bM-^@M-^Mper
claude-M-bM-^@M-.resu-daerM-bM-^@M-,
claudeM-BM--helper
claudeM-bM-^@M-^Khelper
clM-PM-0ude-helper

M-bM-^@M-^K is how cat -v spells the UTF-8 encoding of U+200B (E2 80 8B). Every non-printing byte gets rewritten into that notation, so differences your eyes cannot resolve show up inline. It is the first command I reach for now when a name looks suspicious.

What v2.1.247 closed, and what stays your job

Reading the changelog entry carefully, three separate things changed:

  • The marketplace rejects names that contain control or invisible characters.
  • Marketplace-supplied text in /plugin and claude plugin output is escaped before it is printed.
  • In rendered Markdown, hyperlink targets that point at a network or automounter path, contain a control character, or lead with an invisible character are now shown as plain text instead of a live link.

Both the name itself and the path by which a name reaches your screen were tightened. That is a sensible pair of fixes.

It is, however, a defense at the point where Claude Code ingests something. It does not inspect directory names already sitting on your disk, configuration files you wrote by hand, or the contents of a repository you cloned from GitHub last month. Those were the ones I actually wanted to know about.

Two check functions to run before you install

What I ended up with is two short shell functions. They live in my shell startup file, and I run them right after installing anything new.

# Detect names containing control or invisible characters
scan_names() {
  local dir="${1:?usage: scan_names <dir>}"
  local names hits
  names=$(cd "$dir" && ls -A) || return 2
  hits=$(printf '%s\n' "$names" \
    | grep -nP '[\x00-\x1F\x{00AD}\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{206F}\x{FEFF}]')
  if [ -n "$hits" ]; then
    printf '%s\n' "$hits" | cat -v
    echo "FAIL: $(printf '%s\n' "$hits" | wc -l) name(s) contain control or invisible characters"
    return 1
  fi
  echo "OK: no control or invisible characters found"
}

The character class covers C0 control characters (U+0000–U+001F), the soft hyphen (U+00AD), zero-width and directional formatting characters (U+200B–U+200F and U+202A–U+202E), general punctuation format characters (U+2060–U+206F), and the byte order mark (U+FEFF). The \x{...} syntax belongs to PCRE, so your grep has to be built with --perl-regexp support. The stock macOS grep is not, so either install GNU grep with brew install grep and call ggrep, or use a short Python equivalent instead.

Run against the six test names:

$ scan_names d
2:claude-helM-bM-^@M-^Mper
3:claude-M-bM-^@M-.resu-daerM-bM-^@M-,
4:claudeM-BM--helper
5:claudeM-bM-^@M-^Khelper
FAIL: 4 name(s) contain control or invisible characters
$ echo $?
1

Four of six. Letting the plain name through is correct behavior. The problem is the fifth one that slipped past.

The Cyrillic a walks straight through

The one that escaped is clаude-helper. Its third character is not the Latin a (U+0061) but the Cyrillic а (U+0430). That is not invisible and not a control character — it is an ordinary printable letter in its own right, so no pattern built around invisibility will ever match it.

$ python3 -c "
a='claude-helper'; b='clаude-helper'
print('strings equal:', a == b)
import unicodedata
print('equal after NFKC:', unicodedata.normalize('NFKC',a) == unicodedata.normalize('NFKC',b))
"
strings equal: False
equal after NFKC: False

Unicode normalization does not collapse them either. NFKC folds compatibility characters together, and Cyrillic and Latin letters are deliberately distinct scripts, so this is exactly what the standard promises. Chasing every confusable pair properly means pulling in Unicode's confusables table, which is more machinery than a personal pre-install check deserves.

So I moved the line earlier instead. My assumption is that a plugin or skill name should be printable ASCII and nothing else, and anything outside that gets reported.

# Detect names containing anything outside printable ASCII (catches homoglyphs too)
scan_ascii_only() {
  local dir="${1:?usage: scan_ascii_only <dir>}"
  local hits
  hits=$(cd "$dir" && ls -A | LC_ALL=C grep -n '[^ -~]')
  if [ -n "$hits" ]; then
    printf '%s\n' "$hits" | cat -v
    echo "FAIL: $(printf '%s\n' "$hits" | wc -l) name(s) contain non-ASCII characters"
    return 1
  fi
  echo "OK: everything is printable ASCII"
}

[^ -~] means anything outside the range from space (0x20) to tilde (0x7E). LC_ALL=C keeps the character class from being reinterpreted under a different locale.

$ scan_ascii_only d
2:claude-helM-bM-^@M-^Mper
3:claude-M-bM-^@M-.resu-daerM-bM-^@M-,
4:claudeM-BM--helper
5:claudeM-bM-^@M-^Khelper
6:clM-PM-0ude-helper
FAIL: 5 name(s) contain non-ASCII characters

Five out of five. If you keep directories named in your own language you will see false positives, but scoped to a plugin and skill directory, I find the blunter rule far more usable. A single line with a handful of eyeballed exceptions is a check I will still be running next year. A clever one is not.

Running it on my own 906 entries turned up nothing

Writing the check is meaningless without pointing it at my own machine, so I walked my plugin and skill directories: 906 entries in total counting both files and directories, with the longest name at 43 characters.

Zero names with control or invisible characters. Zero names with any non-ASCII character at all.

Nothing came back. As blog material that is an anticlimax, and I would rather say so plainly. I keep the check anyway, because "verified as zero" and "never looked" are not the same state. The first one turns any future addition into a visible difference. The second one never tells you anything.

One more thing worth passing on: my first version used find with -printf, which is a GNU findutils extension and simply does not exist on macOS. That is why the functions above are built on ls -A. Shipping code that happens to run on the machine you wrote it on is the easiest trap in this whole category.

What to try next

Point scan_ascii_only at your plugin and skill directory once. If it comes back clean, write that down somewhere and move on — that is the whole deliverable. If something does turn up, read the cat -v line and decide whether that character got there on purpose. Either way you have closed a gap that visual inspection was never going to close.

When running it by hand starts to feel tedious, the next step is putting it in a hook. Which of the eight hook types fits which kind of check — and the specific traps I walked into with each — is covered in Claude Code Hooks: A Complete Field Guide to All 8 Hook Types and How to Pick the Right One.

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 $15 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

Claude Code2026-06-25
Your Sandbox Can Run the Code but Shouldn't Read Your Credentials — Shrinking the Secret-Read Surface with sandbox.credentials
Claude Code's sandbox can still read ~/.aws/credentials and token env vars by default. Using sandbox.credentials (v2.1.187+), here is how I tightened the secret-read surface of unattended runs at the OS level, with config and verification you can reuse.
Claude Code2026-05-12
Accidentally Committed API Keys to Git — Emergency Response with Claude Code
Step-by-step emergency guide for when you accidentally commit .env API keys to git. Covers immediate key rotation, removing secrets from git history with Claude Code, and preventing it from happening again.
Claude Code2026-04-25
Secret Management and Trust Boundaries for Claude Code — A Production Guide for the Agent Era
A field-tested approach to secrets in a Claude Code workflow: trust-boundary modeling, three injection patterns, leak-prevention hooks, and rotation runbooks — with working code for .env, MCP, and OS Keychain integrations.
📚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 →