KeygraphHQ/shannon · error · PentestError
Path traversal detected in @include(): ${rawPath}
Error message
Path traversal detected in @include(): ${rawPath} What it means
Thrown by processIncludes when an @include(rawPath) directive in a prompt template resolves to a path outside the allowed base directory. The check resolves rawPath against baseDir and requires the result to either equal resolvedBase or start with resolvedBase + path.sep, blocking directory traversal. This is a security guard preventing prompt files from reading arbitrary files. Category 'prompt', non-retryable.
Source
Thrown at apps/worker/src/services/prompt-manager.ts:260
const errMsg = error instanceof Error ? error.message : String(error);
throw new PentestError(`Failed to build login instructions: ${errMsg}`, 'config', false, {
authentication,
originalError: errMsg,
});
}
}
// Pure function: Process @include() directives
async function processIncludes(content: string, baseDir: string): Promise<string> {
const includeRegex = /@include\(([^)]+)\)/g;
const resolvedBase = path.resolve(baseDir);
const replacements: IncludeReplacement[] = await Promise.all(
Array.from(content.matchAll(includeRegex)).map(async (match) => {
const rawPath = match[1] ?? '';
const includePath = path.resolve(baseDir, rawPath);
if (!includePath.startsWith(resolvedBase + path.sep) && includePath !== resolvedBase) {
throw new PentestError(`Path traversal detected in @include(): ${rawPath}`, 'prompt', false, {
includePath,
baseDir: resolvedBase,
});
}
const sharedContent = await fs.readFile(includePath, 'utf8');
return {
placeholder: match[0],
content: sharedContent,
};
}),
);
for (const replacement of replacements) {
content = replaceLiteral(content, replacement.placeholder, replacement.content);
}
return content;
}
View on GitHub (pinned to 1ae0a142f8)
Solutions
- Open the offending prompt and rewrite the @include path to be relative to the prompts base directory and stay within it (e.g. @include(shared/login-instructions.txt)).
- If you genuinely need a shared partial, place it under the prompts tree (e.g. prompts/shared/) and reference it without .. segments.
- Remove any absolute paths or symlinks that escape the prompts directory from @include directives.
- Re-run the scan after fixing the template.
Example fix
// before: path escapes baseDir // @include(../../configs/secret.yaml) // after: keep the partial inside the prompts tree // @include(shared/_secret-section.txt)
Defensive patterns
Strategy: validation
Validate before calling
// Pre-validate every @include in a prompt stays inside the prompts base dir
import { resolve, relative, sep } from 'node:path';
function includesAreSafe(content: string, baseDir: string): boolean {
const resolvedBase = resolve(baseDir);
for (const m of content.matchAll(/@include\(([^)]+)\)/g)) {
const p = resolve(baseDir, m[1] ?? '');
if (p !== resolvedBase && !p.startsWith(resolvedBase + sep)) return false;
}
return true;
} Type guard
function isSafeIncludePath(rawPath: string, baseDir: string): boolean {
const resolvedBase = resolve(baseDir);
const p = resolve(baseDir, rawPath);
return p === resolvedBase || p.startsWith(resolvedBase + path.sep);
} Try / catch
try {
await processIncludes(template, promptsDir);
} catch (e) {
if (e instanceof PentestError && /Path traversal detected/.test(e.message)) {
// the offending rawPath is in context.includePath — fix the prompt template, do not catch-and-continue
throw new Error(`Unsafe @include in prompt: ${(e.context as any)?.includePath}`);
}
throw e;
} Prevention
- Never accept untrusted user input inside @include directives; treat prompts as trusted code.
- Reference shared partials relative to the prompts base dir, not the including file.
- Avoid absolute paths and '..' segments in @include().
- Audit custom prompts for traversal patterns before mounting them into the worker.
When it happens
Trigger: A prompt template contains an include that escapes the prompts tree: @include(../secrets.env), @include(../../etc/passwd), @include(/absolute/elsewhere), or on Windows a drive/path that resolves outside baseDir. Any user-supplied or edited prompt that uses .. segments or an absolute path in an @include() directive.
Common situations: An operator edits a prompt under apps/worker/prompts/ and adds @include(../shared/login-instructions.txt) thinking the path is relative to the file rather than baseDir (processIncludes resolves relative to baseDir, not the including file). A symlink inside the prompts dir points outside it. A custom prompt accidentally includes an absolute path.
Related errors
- Failed to build login instructions: ${errMsg}
- Template must be a non-empty string
- Variables must include webUrl and repoPath
- Variable interpolation failed: ${errMsg}
- CONFIG_VALIDATION_FAILED
AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12).
Data as JSON: /api/errors/75739133c087071e.
Report an issue: GitHub.