{"record":{"id":"ddc4dabbe6ab16a5","repo":"vercel-labs/skills","slug":"archive-contains-too-many-files-state-entries","errorCode":null,"errorMessage":"Archive contains too many files (${state.entries}). Maximum is ${limits.extractMaxFiles}. Set SKILLS_EXTRACT_MAX_FILES to override.","messagePattern":"Archive contains too many files \\((.+?)\\)\\. Maximum is (.+?)\\. Set SKILLS_EXTRACT_MAX_FILES to override\\.","errorType":"validation","errorClass":"ArchiveValidationError","httpStatus":null,"severity":"error","filePath":"src/download-source.ts","lineNumber":67,"sourceCode":"\nfunction isPathSafe(basePath: string, targetPath: string): boolean {\n  const normalizedBase = normalize(resolve(basePath));\n  const normalizedTarget = normalize(resolve(targetPath));\n  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;\n}\n\nfunction validateArchivePath(path: string): string | null {\n  const normalized = path.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n  if (!normalized || normalized.endsWith('/')) return normalized;\n  if (normalized.startsWith('/') || /^[a-zA-Z]:\\//.test(normalized)) return null;\n  if (normalized.split('/').includes('..')) return null;\n  return normalized;\n}\n\nfunction incrementEntry(state: ExtractState, size: number, limits: DownloadLimits): void {\n  state.entries += 1;\n  if (state.entries > limits.extractMaxFiles) {\n    throw new ArchiveValidationError(\n      `Archive contains too many files (${state.entries}). Maximum is ${limits.extractMaxFiles}. Set SKILLS_EXTRACT_MAX_FILES to override.`\n    );\n  }\n\n  state.bytes += size;\n  if (state.bytes > limits.extractMaxBytes) {\n    throw new ArchiveValidationError(\n      `Archive extracts to more than ${limits.extractMaxBytes} bytes. Set SKILLS_EXTRACT_MAX_BYTES to override.`\n    );\n  }\n}\n\nasync function downloadToFile(\n  url: string,\n  targetFile: string,\n  limits: DownloadLimits\n): Promise<void> {\n  const response = await fetch(url, {","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/vercel-labs/skills/blob/435076e78988e1e6ec40d00b0b1d76bdbbc5419a/src/download-source.ts#L49-L85","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Raise the cap: SKILLS_EXTRACT_MAX_FILES=5000 skills add <source>","Inspect the archive entry count first (unzip -l file.zip | tail -1) and prune it if it contains unrelated files","Prefer installing from a git source or local path, which bypasses archive extraction limits","Only raise the limit for trusted sources — it exists to stop zip bombs"],"exampleFix":"# before\nskills add ./huge-bundle.zip  # too many files\n\n# after\nSKILLS_EXTRACT_MAX_FILES=10000 skills add ./huge-bundle.zip","handlingStrategy":"validation","validationCode":"// Count entries before extracting\nimport { execFileSync } from 'node:child_process';\nfunction zipEntryCount(path: string): number {\n  const out = execFileSync('unzip', ['-l', path], { encoding: 'utf8' });\n  const m = out.match(/(\\d+) files?/);\n  return m ? Number(m[1]) : -1;\n}","typeGuard":"null","tryCatchPattern":"try {\n  await extractArchive(file, limits);\n} catch (err) {\n  if (err instanceof ArchiveValidationError && err.message.includes('too many files')) {\n    process.env.SKILLS_EXTRACT_MAX_FILES = String(limit * 10); // retry with raised cap if trusted\n  } else throw err;\n}","preventionTips":["Only raise SKILLS_EXTRACT_MAX_FILES for archives you built yourself","Prefer git or local-path sources for large bundles"],"tags":["zip","limits","zip-bomb","env-var","security"],"backgroundTag":"resource-limit-exceeded","analyzedSha":"435076e78988e1e6ec40d00b0b1d76bdbbc5419a","analyzedAt":"2026-08-28T17:47:53.369Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}