nexu-io/open-design · error
project_files.read_json does not follow symlinks
Error message
project_files.read_json does not follow symlinks
What it means
executeProjectFilesReadJson (refresh.ts:590-591) calls lstat on the resolved target and refuses to proceed if it is a symbolic link. This prevents a symlink from redirecting the read outside the validated project directory after the realpath boundary check would be bypassed.
Source
Thrown at apps/daemon/src/live-artifacts/refresh.ts:592
size: file.size,
mtime: file.mtime,
kind: file.kind ?? 'file',
mime: file.mime ?? 'application/octet-stream',
};
if (preview !== undefined) result.preview = preview;
matches.push(result);
}
return asBoundedRefreshOutput({ toolName: 'project_files.search', query: query ?? '', count: matches.length, truncated: allFiles.length > matches.length && matches.length >= maxResults, matches });
}
async function executeProjectFilesReadJson(options: ExecuteLocalDaemonRefreshSourceOptions): Promise<BoundedJsonObject> {
const filePath = selectJsonPath(options.source.input as ProjectFilesReadJsonInput);
if (!filePath.endsWith('.json')) throw new Error('project_files.read_json only supports .json files');
const dir = projectDir(options.projectsRoot, options.projectId);
const target = path.resolve(dir, filePath);
const [dirReal, targetLinkStat] = await Promise.all([realpath(dir), lstat(target)]);
if (targetLinkStat.isSymbolicLink()) throw new Error('project_files.read_json does not follow symlinks');
const targetReal = await realpath(target);
if (!targetReal.startsWith(`${dirReal}${path.sep}`) && targetReal !== dirReal) {
throw new Error('project_files.read_json path escapes project dir');
}
const entryStat = await stat(targetReal);
if (!entryStat.isFile()) throw new Error('project_files.read_json path must be a file');
if (entryStat.size > 256 * 1024) throw new Error('project_files.read_json file exceeds 256KB');
if (options.signal?.aborted === true) throw options.signal.reason;
let parsed: BoundedJsonValue;
try {
parsed = JSON.parse(await readFile(targetReal, 'utf8')) as BoundedJsonValue;
} catch {
throw new Error(`project_files.read_json could not parse JSON at ${filePath}`);
}
return asBoundedRefreshOutput({ toolName: 'project_files.read_json', path: filePath, size: entryStat.size, json: parsed });
}
function compactExecOutput(value: string): string[] {View on GitHub (pinned to 5be4028344)
Solutions
- Read the real file directly instead of through a symlink (place a real copy under the project dir).
- Remove or replace the symlink with the actual JSON file.
- If the link is intentional and trusted, expose its target as a normal file in the project.
Example fix
// before: project has data.json -> /shared/data.json (symlink)
const input = { path: 'data.json' };
// throws `project_files.read_json does not follow symlinks`
// after: replace the symlink with a real file
// cp /shared/data.json ./data.json (regular file) Defensive patterns
Strategy: validation
Validate before calling
import { lstat } from 'node:fs/promises';
async function assertNotSymlink(target: string): Promise<void> {
const st = await lstat(target);
if (st.isSymbolicLink()) {
throw new Error('project_files.read_json does not follow symlinks');
}
} Prevention
- Keep real (non-symlinked) JSON files in the project for read_json sources.
- When staging project files, copy rather than symlink data files.
- Audit project trees for symlinks before pointing read_json at them.
When it happens
Trigger: input.path resolves (via path.resolve) to a filesystem entry whose lstat reports it as a symbolic link, even if the link target is inside the project.
Common situations: A project contains symlinks (e.g. node_modules-style linking, monorepo workspace links, a symlinked config file); a user symlinked a shared data file into the project.
Related errors
- project_files.read_json path escapes project dir
- BAD_REQUEST
- path escapes project dir
- ${field} contains unsupported JSON path segment: ${segment}
- project_files.read_json requires input.path
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/a5be4d3915713a12.
Report an issue: GitHub.