affaan-m/ECC · error · Error
${source} is missing the expected catalog marker
Error message
${source} is missing the expected catalog marker What it means
_remove_project_storage() is a defense-in-depth guard around shutil.rmtree. It resolves both PROJECTS_DIR and the target (PROJECTS_DIR / project_id) and refuses to delete if the resolved target equals the root itself or is not a descendant of it. This prevents a path-traversal project_id (or a symlinked project directory, or a future caller with a relaxed validator) from turning the recursive delete into an arbitrary-directory removal.
Source
Thrown at scripts/ci/catalog.js:82
function readFileOrThrow(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
throw new Error(`Failed to read ${path.basename(filePath)}: ${error.message}`);
}
}
function writeFileOrThrow(filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf8');
} catch (error) {
throw new Error(`Failed to write ${path.basename(filePath)}: ${error.message}`);
}
}
function replaceOrThrow(content, regex, replacer, source) {
if (!regex.test(content)) {
throw new Error(`${source} is missing the expected catalog marker`);
}
return content.replace(regex, replacer);
}
function parseReadmeExpectations(readmeContent) {
const expectations = [];
const quickStartMatch = readmeContent.match(
/access to\s+(\d+)\s+agents,\s+(\d+)\s+skills,\s+and\s+(\d+)\s+(?:commands|legacy command shims?)/i
);
if (!quickStartMatch) {
throw new Error('README.md is missing the quick-start catalog summary');
}
expectations.push(
{ category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'README.md quick-start summary' },
{ category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'README.md quick-start summary' },View on GitHub (pinned to 01e15490f0)
Solutions
- Validate project_id upstream with _validate_instinct_id (alphanumeric + ._-, no '/', '\', or '..').
- Remove any symlinks inside PROJECTS_DIR before calling, or reject symlinked project directories.
- Never pass an empty, None, or absolute-path project_id to this function.
Example fix
# before
_remove_project_storage(project_id) # project_id = '../evil'
# after
if not _validate_instinct_id(project_id):
raise ValueError(f'invalid project id: {project_id!r}')
_remove_project_storage(project_id) Defensive patterns
Strategy: validation
Validate before calling
# Validate the project_id before calling _remove_project_storage.
if not _validate_instinct_id(project_id):
raise ValueError(f'refusing to remove storage: invalid project_id {project_id!r}')
_remove_project_storage(project_id) Type guard
# Reuse the existing validator as a type guard.
from pathlib import Path
def is_safe_project_id(project_id: str) -> bool:
return (
bool(project_id)
and len(project_id) <= 128
and '/' not in project_id
and '\\' not in project_id
and '..' not in project_id
and not project_id.startswith('.')
) Try / catch
try:
_remove_project_storage(project_id)
except ValueError as e:
if 'escapes' in str(e):
log.error('refused to delete: project_id resolves outside PROJECTS_DIR — possible traversal')
raise Prevention
- Always validate project_id with _validate_instinct_id before any storage operation.
- Reject empty, None, or absolute-path project IDs at the input boundary.
- Periodically check for and remove symlinks inside PROJECTS_DIR.
- Treat an 'escapes' error as a security incident, not a routine failure.
When it happens
Trigger: project_id contains '../' that resolves outside PROJECTS_DIR; project_id is empty or '.' so project_dir == projects_root; a symlink inside PROJECTS_DIR/<id> points outside the root; project_id contains an absolute path.
Common situations: Upstream project_id validation was bypassed or relaxed; a symlink was placed in the projects directory; an empty/None project_id reached this function; a test injected a traversal string.
Related errors
- TypeScript compiler not found. Install root dev dependencies
- ${EXECUTABLE_OVERRIDE} does not point to a readable local It
- Refusing to read non-file path: ${filePath}
- ${label} must be a regular, non-symlink file.
- seedPaths entries must stay inside repoRoot: ${entry}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/305c69c0ec77c0f6.
Report an issue: GitHub.