NousResearch/hermes-agent · error
${caller}: illegal path traversal in "${path}"
Error message
${caller}: illegal path traversal in "${path}" What it means
Thrown by pluginPathSuffix() in apps/desktop/src/hermes.ts:285 when a `path` passed to the plugin REST door contains a `..` segment. pluginRest() scopes every request by construction to `/api/plugins/<pluginId>`; the `..` rejection is what prevents a relative path from normalizing up into another plugin's API namespace or a core route. Only the path portion (before any `?query` or `#hash`) is checked.
Source
Thrown at apps/desktop/src/hermes.ts:285
/** Options for a plugin REST call — mirrors the app's own `hermesDesktop.api`
* shape, minus the path (which is namespace-derived). */
export interface PluginRestOptions {
method?: string
body?: unknown
/** Single-file multipart upload (see HermesApiRequest.upload). */
upload?: { filename: string; contentType?: string; bytes: ArrayBuffer }
timeoutMs?: number
}
// Normalize `path` to a leading-slash suffix relative to `/api/plugins/<id>`.
// The namespace is the boundary — reject `..` so a relative segment can't
// normalize out into another plugin's API or a core route. Check the path
// portion only (before any query/hash).
function pluginPathSuffix(caller: string, path: string): string {
const suffix = path.startsWith('/') ? path : `/${path}`
if (suffix.split(/[?#]/, 1)[0].split('/').includes('..')) {
throw new Error(`${caller}: illegal path traversal in "${path}"`)
}
return suffix
}
/** The plugin REST door. Every call is scoped BY CONSTRUCTION to the plugin's
* own backend namespace — `path` is relative to `/api/plugins/<pluginId>`
* ('/board' → `/api/plugins/kanban/board`), so a plugin can't address another
* plugin's API or a core route through it. Profile-aware like every desktop
* REST call. Broader reach (core endpoints, another namespace) is the future
* declared-capability seam; today the namespace IS the boundary. */
export async function pluginRest<T>(pluginId: string, path: string, opts: PluginRestOptions = {}): Promise<T> {
if (!window.hermesDesktop?.api) {
throw new Error('Hermes desktop bridge unavailable')
}
const suffix = pluginPathSuffix('pluginRest', path)
View on GitHub (pinned to c896c09c42)
Solutions
- Keep pluginRest paths strictly inside the plugin's own namespace and always start them with '/' (e.g. '/board', '/items/42').
- Sanitize user/agent-supplied segments before interpolation: reject or encodeURIComponent anything containing '..' as a segment.
- If you genuinely need another plugin's API or a core route, do not use pluginRest — call the appropriate dedicated API helper; the namespace IS the boundary.
- Strip traversal at the source: normalize the path first and assert no '..' segment remains before calling.
Example fix
// before
await pluginRest('kanban', `../settings/all`)
await pluginRest('kanban', `/board/${userInput}`) // userInput = '../../x'
// after
await hermesApi('/api/settings/all') // core route via its own helper
await pluginRest('kanban', `/board/${encodeURIComponent(userInput)}`) Defensive patterns
Strategy: validation
Validate before calling
function safePluginPath(path: string): string {
const suffix = path.startsWith('/') ? path : `/${path}`
const pathOnly = suffix.split(/[?#]/, 1)[0]
if (pathOnly.split('/').includes('..')) throw new Error(`illegal path: ${path}`)
return suffix
}
const clean = `/items/${encodeURIComponent(userSuppliedId)}`
safePluginPath(clean) Try / catch
try { await pluginRest(id, path) } catch (e) { if (e instanceof Error && e.message.includes('illegal path traversal')) { /* fix path construction; do NOT strip '..' blindly and retry */ } } Prevention
- Build plugin paths from a fixed table, not string concatenation of user input
- encodeURIComponent every interpolated segment
- Never attempt cross-plugin or core routes through pluginRest
- Treat hitting this error as a security signal, log it — don't silently normalize
When it happens
Trigger: Calling pluginRest(pluginId, '../other-plugin/board'); building a path by concatenation that yields '/a/../b'; passing a user-supplied relative path like 'items/../../admin' into a plugin REST helper; `..` appearing as a whole segment (`'a/..b'` is fine, `'a/..'` is not).
Common situations: A plugin naively joining ids into a URL: `${base}/${id}` where id contains traversal; UI code forwarding a typed route that includes parent-directory shorthand; attempting to reach a sibling plugin's endpoint or a core endpoint through the namespaced door.
Related errors
- integrity check failed for ${origin}
- Not a directory: ${watchDir}
- File uploads are not supported against OAuth-gated remote ba
- Invalid external URL
- Invalid preview URL
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/23356cf23c872363.
Report an issue: GitHub.