vercel-labs/skills · error · ArchiveValidationError

Archive contains too many files (${state.entries}). Maximum

Error message

Archive contains too many files (${state.entries}). Maximum is ${limits.extractMaxFiles}. Set SKILLS_EXTRACT_MAX_FILES to override.

What it means

While streaming archive entries, incrementEntry counts each file and throws ArchiveValidationError once state.entries exceeds limits.extractMaxFiles. This is a zip-bomb / resource-exhaustion guard: extraction stops before the archive can create an unbounded number of files. The cap is configurable via the SKILLS_EXTRACT_MAX_FILES environment variable.

Source

Thrown at src/download-source.ts:67

function isPathSafe(basePath: string, targetPath: string): boolean {
  const normalizedBase = normalize(resolve(basePath));
  const normalizedTarget = normalize(resolve(targetPath));
  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
}

function validateArchivePath(path: string): string | null {
  const normalized = path.replace(/\\/g, '/').replace(/^\.\//, '');
  if (!normalized || normalized.endsWith('/')) return normalized;
  if (normalized.startsWith('/') || /^[a-zA-Z]:\//.test(normalized)) return null;
  if (normalized.split('/').includes('..')) return null;
  return normalized;
}

function incrementEntry(state: ExtractState, size: number, limits: DownloadLimits): void {
  state.entries += 1;
  if (state.entries > limits.extractMaxFiles) {
    throw new ArchiveValidationError(
      `Archive contains too many files (${state.entries}). Maximum is ${limits.extractMaxFiles}. Set SKILLS_EXTRACT_MAX_FILES to override.`
    );
  }

  state.bytes += size;
  if (state.bytes > limits.extractMaxBytes) {
    throw new ArchiveValidationError(
      `Archive extracts to more than ${limits.extractMaxBytes} bytes. Set SKILLS_EXTRACT_MAX_BYTES to override.`
    );
  }
}

async function downloadToFile(
  url: string,
  targetFile: string,
  limits: DownloadLimits
): Promise<void> {
  const response = await fetch(url, {

View on GitHub (pinned to 435076e789)

Solutions

  1. Raise the cap: SKILLS_EXTRACT_MAX_FILES=5000 skills add <source>
  2. Inspect the archive entry count first (unzip -l file.zip | tail -1) and prune it if it contains unrelated files
  3. Prefer installing from a git source or local path, which bypasses archive extraction limits
  4. Only raise the limit for trusted sources — it exists to stop zip bombs

Example fix

# before
skills add ./huge-bundle.zip  # too many files

# after
SKILLS_EXTRACT_MAX_FILES=10000 skills add ./huge-bundle.zip
Defensive patterns

Strategy: validation

Validate before calling

// Count entries before extracting
import { execFileSync } from 'node:child_process';
function zipEntryCount(path: string): number {
  const out = execFileSync('unzip', ['-l', path], { encoding: 'utf8' });
  const m = out.match(/(\d+) files?/);
  return m ? Number(m[1]) : -1;
}

Type guard

null

Try / catch

try {
  await extractArchive(file, limits);
} catch (err) {
  if (err instanceof ArchiveValidationError && err.message.includes('too many files')) {
    process.env.SKILLS_EXTRACT_MAX_FILES = String(limit * 10); // retry with raised cap if trusted
  } else throw err;
}

Prevention

When it happens

Trigger: Extracting an archive containing more files than the configured limit (default cap set by DownloadLimits), e.g. a monorepo snapshot or a skill bundle with thousands of entries. Also triggered maliciously by a zip-bomb designed to exhaust inodes/disk.

Common situations: Adding a large multi-skill repository as a zip instead of via git, vendored archives with node_modules included, or environments where someone lowered SKILLS_EXTRACT_MAX_FILES and forgot.

Related errors


AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28). Data as JSON: /api/errors/ddc4dabbe6ab16a5. Report an issue: GitHub.