CLAUDE LABJP
MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attributionMCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Articles/Claude Code
Claude Code/2026-08-06Advanced

The Skill Call That Returned Nothing — Pinning Down Name Resolution Before an Unattended Run

A skill whose name collides with a built-in terminal command may never resolve in a non-interactive session. Here is a preflight that builds the effective-name table before launch, plus the measured run where the checker itself silently skipped two entries.

Claude Code225Skill DesignUnattended Automation3Preflight2Namespaces

Premium Article

I opened the logs from an overnight scheduled run and found almost nothing in them.

No failure. No stack trace. No timeout record. The step I had wired up simply never executed.

Before I found the real cause, I spent three hours suspecting permission settings and network allowlists. Both were innocent. What actually happened was far more mundane: a skill I was distributing through a plugin shared its name with a built-in terminal command.

In an interactive session, typing that name gets you the built-in. It looks like everything works. In a non-interactive session, the same call resolves to nobody at all, and falls on the floor without a sound.

That particular defect was fixed in the August 6, 2026 update. But what got fixed is a symptom, not the shape of the problem. Call names arrive from several sources, and the effective name is decided by a resolution order. If you name a skill without knowing that order, you will break the same way again — just as quietly.

So I wrote a tool that builds the effective-name table before launch. Then I ran it, and found something the tool itself had been missing.

Call names are a resolution order, not a list

Skills come from more than one place. The project directory. Your home directory. Plugins you have installed. In an organization, add whatever an administrator distributes on top of that.

It is tempting to think of this as "the list of skills I have available." It is not a list. It is a single namespace that several independent sources write into.

SourceLocationWho changes itWill you notice a change?
Built-in commandsThe terminal itselfThe vendorOnly if you read the changelog
ProjectInside the repositoryYou and your teamYes, it shows in the diff
UserUnder your home directoryYouNot if it is outside version control
PluginInstalled pluginsWhoever publishes itAlmost never

The bottom two rows are where the trouble lives. Skills under your home directory never appear in a repository diff. Skills provided by a plugin enter your namespace the moment the publisher adds a new name, without asking you.

Which means whether your own skill still answers to its name tomorrow is not entirely your decision.

Built-in commands sit on top of all of this. They win, and they change fastest. In the first week of August alone, new manually invocable skill entry points were added. A name that was free yesterday can be occupied today.

Why interactive and non-interactive behave differently

In an interactive session, whatever you type passes through the terminal's input layer first. If it matches a built-in command, resolution stops there. Since a human is doing the typing, a missing response registers immediately.

A non-interactive session has no such input layer. The call goes to the agent's own resolution path. When the name is reserved on the built-in side, the agent's skill table and the vendor's reserved words disagree, and the call is left hanging.

And in unattended execution, nobody is around to complain about a hanging call.

The tell I missed for three hours: a permission problem leaves a denial record. The complete absence of any record was itself the evidence that permissions were not the cause.

Building the effective-name table before launch

What I needed was a way to answer, on every run, a single question: if I call this name, which file actually executes?

I settled on three rules.

  1. Scan every source and collapse the results into one effective-name table.
  2. Treat a collision with a built-in command as a stop condition, not a warning.
  3. Treat any source that could not be scanned as a failure in its own right.

The third rule turned out to matter more than I expected.

Here is the implementation. It uses only Node.js standard modules — adding a dependency invites the failure where the package is simply absent in the unattended environment.

#!/usr/bin/env node
// name-preflight.mjs — resolve skill call names before launch and block collisions
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
import { join, basename } from "node:path";
 
// Built-in terminal commands. Derive this from /help in your own environment
// and keep it under version control as a fixed list.
const BUILTIN = new Set([
  "help", "clear", "compact", "config", "cost", "doctor", "exit",
  "init", "login", "logout", "mcp", "memory", "model", "permissions",
  "release-notes", "resume", "review", "rewind", "status", "vim",
]);
 
// Lower rank resolves first. Always overwrite this with what you observe
// in your own environment.
const SOURCES = [
  { id: "project", rank: 1, root: ".claude/skills" },
  { id: "user",    rank: 2, root: join(process.env.HOME ?? "", ".claude/skills") },
  { id: "plugin",  rank: 3, root: ".claude/plugins", nested: true },
];
 
const missingRoots = [];
 
function readName(dir) {
  const f = join(dir, "SKILL.md");
  if (!existsSync(f)) return null;
  const head = readFileSync(f, "utf8").split(/\n---\s*\n/)[0];
  const m = head.match(/^name:\s*(.+)$/m);
  return (m ? m[1] : basename(dir)).trim().replace(/^["']|["']$/g, "");
}
 
function scanFlat(root, source, owner) {
  if (!existsSync(root)) { missingRoots.push(`${source}:${root}`); return []; }
  return readdirSync(root)
    .map((e) => join(root, e))
    .filter((p) => statSync(p).isDirectory())
    .map((p) => ({ name: readName(p), source, owner, path: p }))
    .filter((r) => r.name);
}
 
function collect() {
  const out = [];
  for (const s of SOURCES) {
    if (!s.nested) { out.push(...scanFlat(s.root, s.id, s.id)); continue; }
    if (!existsSync(s.root)) { missingRoots.push(`${s.id}:${s.root}`); continue; }
    for (const plug of readdirSync(s.root)) {
      out.push(...scanFlat(join(s.root, plug, "skills"), s.id, plug));
    }
  }
  return out;
}
 
function resolve(entries) {
  const rank = Object.fromEntries(SOURCES.map((s) => [s.id, s.rank]));
  const byName = new Map();
  for (const e of entries) {
    const list = byName.get(e.name) ?? [];
    list.push(e);
    byName.set(e.name, list);
  }
  const findings = [];
  for (const [name, list] of byName) {
    list.sort((a, b) => rank[a.source] - rank[b.source]);
    const winner = list[0];
    if (BUILTIN.has(name)) {
      findings.push({
        level: "block", name,
        reason: "collides with a built-in command; may never resolve in a non-interactive session",
        effective: "builtin", losers: list,
      });
      continue;
    }
    if (list.length > 1) {
      findings.push({
        level: "warn", name,
        reason: `${list.length} skills share this name; ${winner.source}(${winner.owner}) wins`,
        effective: `${winner.source}:${winner.owner}`, losers: list.slice(1),
      });
    }
  }
  return { total: entries.length, unique: byName.size, findings };
}
 
const t0 = process.hrtime.bigint();
const r = resolve(collect());
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
 
const blocks = r.findings.filter((f) => f.level === "block");
const warns  = r.findings.filter((f) => f.level === "warn");
for (const m of missingRoots) console.log(`[GAP] scan target does not exist: ${m}`);
console.log(`scanned=${r.total} unique=${r.unique} block=${blocks.length} warn=${warns.length} in ${ms.toFixed(1)}ms`);
for (const f of [...blocks, ...warns]) {
  console.log(`[${f.level.toUpperCase()}] /${f.name} -> ${f.effective}`);
  console.log(`        ${f.reason}`);
  for (const l of f.losers) console.log(`        shadowed: ${l.source}:${l.owner} (${l.path})`);
}
process.exit(blocks.length > 0 || missingRoots.length > 0 ? 1 : 0);

BUILTIN is a hardcoded constant on purpose. If you query the live list of built-in commands instead, a failed query hands you a comforting "zero collisions." A dull list in version control, reviewed by eye whenever the tool updates, is more trustworthy in an unattended context.

The same reasoning applies to rank. Guessing at the precedence order is not good enough. Create a deliberate collision in your own environment, observe which side answers, and overwrite the values. What the code contributes is that the order is fixed and written down — not that my particular ordering is correct for you.

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
A ~90-line Node.js preflight that blocks built-in name collisions before launch, with measured output from a 10-skill tree
How one unscanned source root changes the finding count from 4 to 5, and how to make missing roots fail loudly instead of silently
Why a directory-name search finds 1 match where the effective-name count is 2, plus a measured 18ms scan across 500 skills
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-18
I Believed Plan Mode Only Read — Replacing That Belief With Machinery
Claude Code 2.1.212 fixed a bug where plan mode ran file-modifying Bash commands without the permission prompt or the SDK canUseTool callback. Here is what could happen while that assumption was broken, how to verify your own setup, and how to stop leaning on a mode name for safety.
Claude Code2026-06-28
When You Fan Out Streaming Sessions, Your Laptop's CPU Gives Out First — An Adaptive Throttle That Caps Concurrency by Measured Load
Even with lighter streaming, fanning out many sessions on one machine saturates the host CPU before anything else. Here is why a fixed semaphore fails, plus a working adaptive gate that raises and lowers concurrency from measured CPU.
Claude Code2026-08-19
Fixing the Code Doesn't Evict a Broken Page From the Edge Cache
I shipped a fix and the broken page kept serving. The problem was not the code but the edge cache, which had stored a broken HTML response because the store decision looked only at the status code. Here is the integrity guard I now run before every cache write, and the thresholds I had to tune in production.
📚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 →