{"record":{"id":"875968daf067322e","repo":"google-gemini/gemini-cli","slug":"path-validation-failed-patherror","errorCode":null,"errorMessage":"Path validation failed: ${pathError}","messagePattern":"Path validation failed: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/a2a-server/src/agent/task.ts","lineNumber":667,"sourceCode":"      kind: 'message',\n      role: 'agent',\n      parts: messageParts,\n      messageId: uuidv4(),\n      taskId,\n      contextId,\n    };\n  }\n\n  private async getProposedContent(\n    file_path: string,\n    old_string: string,\n    new_string: string,\n  ): Promise<string> {\n    // Validate path to prevent path traversal vulnerabilities\n    const resolvedPath = path.resolve(this.config.getTargetDir(), file_path);\n    const pathError = this.config.validatePathAccess(resolvedPath, 'read');\n    if (pathError) {\n      throw new Error(`Path validation failed: ${pathError}`);\n    }\n\n    try {\n      const rawContent = await fs.readFile(resolvedPath, 'utf8');\n      const hasCrlf = rawContent.includes('\\r\\n');\n      const currentContent = rawContent.replace(/\\r\\n/g, '\\n');\n      const normalizedOldString = old_string.replace(/\\r\\n/g, '\\n');\n      const normalizedNewString = new_string.replace(/\\r\\n/g, '\\n');\n      const proposedContent = this._applyReplacement(\n        currentContent,\n        normalizedOldString,\n        normalizedNewString,\n        normalizedOldString === '' && currentContent === '',\n      );\n      return hasCrlf ? proposedContent.replace(/\\n/g, '\\r\\n') : proposedContent;\n    } catch (err) {\n      if (!isNodeError(err) || err.code !== 'ENOENT') throw err;\n      return '';","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/5024443c7217464a66e98f80d73172a26440bd8f/packages/a2a-server/src/agent/task.ts#L649-L685","documentation":"Thrown by Task.getProposedContent (used by the edit/replace tool) when config.validatePathAccess(resolvedPath, 'read') returns a non-null error string. validatePathAccess is the sandbox/path-traversal guard from gemini-cli-core's PathValidator: it rejects paths outside the allowed root, forbidden extensions, symlink escapes, and permission failures. The resolved path is computed by path.resolve(targetDir, file_path) so relative file_path values are bound to the workspace target dir.","triggerScenarios":"The edit tool is invoked with a file_path that, after resolution, falls outside the workspace sandbox; the path points at a forbidden extension or a symlink that escapes the allowed root; the underlying directory lacks read permission. validatePathAccess returns a reason string which is interpolated into the message.","commonSituations":"Agent emits an edit with an absolute path (/etc/hosts) or a traversal (../../secret); workspace target dir misconfigured so legitimate files appear outside the root; sandbox extension allowlist excludes the file type; symlink inside the workspace points outside.","solutions":["Read the interpolated pathError - it states the specific reason (outside root, forbidden extension, permission).","If the path is legitimately within scope, check that config.getTargetDir() and the sandbox root/extension allowlist match the workspace you intend.","Avoid passing absolute paths; use paths relative to the workspace root.","If a symlink is involved, confirm the PathValidator extension config permits following it."],"exampleFix":"// before\nconst resolvedPath = path.resolve(this.config.getTargetDir(), file_path);\nconst pathError = this.config.validatePathAccess(resolvedPath, 'read');\nif (pathError) throw new Error(`Path validation failed: ${pathError}`);\n\n// after (caller-side: pre-validate and surface a tool error instead of throwing)\nconst pathError = this.config.validatePathAccess(resolvedPath, 'read');\nif (pathError) {\n  return { status: 'failed', error: `Path not allowed: ${pathError}` };\n}","handlingStrategy":"validation","validationCode":"import path from 'node:path';\n\nfunction assertEditPathSafe(config: { getTargetDir(): string; validatePathAccess(p: string, mode: 'read'|'write'): string | null }, filePath: string) {\n  const resolved = path.resolve(config.getTargetDir(), filePath);\n  const err = config.validatePathAccess(resolved, 'read');\n  if (err) throw new Error(`Refusing edit: ${err}`);\n}\n// call before constructing the edit tool call","typeGuard":null,"tryCatchPattern":"try {\n  return await task.getProposedContent(file_path, old_string, new_string);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Path validation failed: ')) {\n    return { status: 'failed', error: e.message } as const;\n  }\n  throw e;\n}","preventionTips":["Always pass file_path relative to the workspace root, never absolute paths from arbitrary user input.","Keep the PathValidator extension allowlist and sandbox root in sync with config.getTargetDir().","Reject edit tool calls whose path contains '..' at the dispatch layer before they reach getProposedContent.","Audit symlinks inside the workspace - dereferenced targets must remain under the allowed root."],"tags":["path-validation","sandbox","security","edit-tool","path-traversal"],"backgroundTag":null,"analyzedSha":"5024443c7217464a66e98f80d73172a26440bd8f","analyzedAt":"2026-08-12T06:01:53.711Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}