CLAUDE LABJP
2.1.278 — The auto mode classifier now runs server-side by default on the Claude API, Enterprise, Bedrock, Vertex and Foundry. You are not billed for the classifier, and /status gained an Auto mode server lineTASKOUT — The TaskOutput tool is gone. taskOutputMaxChars and TASK_MAX_OUTPUT_LENGTH no longer do anything, and background output is read with Read instead10/07 — The old management-configuration key spellings are accepted until noon PT on October 7, seventeen days from now. After that, entries that still use them stop working until you rewrite themBUNPANIC — Reports are coming in of the newest build crashing on launch alone. Earlier builds still run on the same machine, which points at the release rather than the environmentNEW — Deciding what belongs in Cowork and what belongs in Claude Code, using the approval boundary as the lineSONNET4.5 — A date in a deprecation table is a floor, not an end date. Sonnet 4.5 is still active and no deprecation notice has been posted2.1.278 — The auto mode classifier now runs server-side by default on the Claude API, Enterprise, Bedrock, Vertex and Foundry. You are not billed for the classifier, and /status gained an Auto mode server lineTASKOUT — The TaskOutput tool is gone. taskOutputMaxChars and TASK_MAX_OUTPUT_LENGTH no longer do anything, and background output is read with Read instead10/07 — The old management-configuration key spellings are accepted until noon PT on October 7, seventeen days from now. After that, entries that still use them stop working until you rewrite themBUNPANIC — Reports are coming in of the newest build crashing on launch alone. Earlier builds still run on the same machine, which points at the release rather than the environmentNEW — Deciding what belongs in Cowork and what belongs in Claude Code, using the approval boundary as the lineSONNET4.5 — A date in a deprecation table is a floor, not an end date. Sonnet 4.5 is still active and no deprecation notice has been posted
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 Code255Skill 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
Two alarm-side defects found after a month in production (permanent exit 1 from a commands-only plugin; a broken symlink crashing the checker) and the fix that splits exit codes into 0/2/3
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 $15 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-09-20
I Was Paying for Pro and Still Getting Billed by the Console
Your subscription and your API usage are two separate ledgers, and Claude Code will happily run on either one. Which credential wins is decided by a precedence list where your login sits dead last. Here is how to read status, and why unattended runs answer differently.
📚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